Skip to content

Commit

Permalink
Merge branch 'feature/confirmationdialog' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
peterfriese committed Nov 26, 2021
2 parents 3a84b6d + 61f95f6 commit ccc55d4
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 31 deletions.
8 changes: 8 additions & 0 deletions code/frontend/MakeItSo/MakeItSo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
88C30CD7274D1B4500E6694D /* ReminderDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C30CD5274D1B4500E6694D /* ReminderDetailsViewModel.swift */; };
88E7B7BE274CF30A00AF477D /* ReminderDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E7B7BD274CF30A00AF477D /* ReminderDetailsView.swift */; };
88E7B7BF274CF30A00AF477D /* ReminderDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E7B7BD274CF30A00AF477D /* ReminderDetailsView.swift */; };
88FA998D274D63A400670474 /* View+ConfirmationDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FA998C274D63A400670474 /* View+ConfirmationDialog.swift */; };
88FA99922750090200670474 /* View+InteractiveDismissDisable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FA99912750090200670474 /* View+InteractiveDismissDisable.swift */; };
88FEECDA27275ABD00ED368C /* MakeItSoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FEECCA27275ABC00ED368C /* MakeItSoApp.swift */; };
88FEECDB27275ABD00ED368C /* MakeItSoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FEECCA27275ABC00ED368C /* MakeItSoApp.swift */; };
88FEECDE27275ABD00ED368C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88FEECCC27275ABD00ED368C /* Assets.xcassets */; };
Expand All @@ -33,6 +35,8 @@
887B6FA9273ED4180028263D /* EmptyStateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = "<group>"; };
88C30CD5274D1B4500E6694D /* ReminderDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderDetailsViewModel.swift; sourceTree = "<group>"; };
88E7B7BD274CF30A00AF477D /* ReminderDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderDetailsView.swift; sourceTree = "<group>"; };
88FA998C274D63A400670474 /* View+ConfirmationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ConfirmationDialog.swift"; sourceTree = "<group>"; };
88FA99912750090200670474 /* View+InteractiveDismissDisable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+InteractiveDismissDisable.swift"; sourceTree = "<group>"; };
88FEECCA27275ABC00ED368C /* MakeItSoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MakeItSoApp.swift; sourceTree = "<group>"; };
88FEECCC27275ABD00ED368C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
88FEECD127275ABD00ED368C /* MakeItSo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MakeItSo.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -66,6 +70,8 @@
isa = PBXGroup;
children = (
881EF5BC272DC399004761E5 /* View+Focus.swift */,
88FA998C274D63A400670474 /* View+ConfirmationDialog.swift */,
88FA99912750090200670474 /* View+InteractiveDismissDisable.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -272,7 +278,9 @@
buildActionMask = 2147483647;
files = (
88FEECF02727FEFF00ED368C /* Reminder.swift in Sources */,
88FA998D274D63A400670474 /* View+ConfirmationDialog.swift in Sources */,
88FEECFA27280F3D00ED368C /* ReminderListRowView.swift in Sources */,
88FA99922750090200670474 /* View+InteractiveDismissDisable.swift in Sources */,
887B6FAD273ED4180028263D /* EmptyStateView.swift in Sources */,
88E7B7BE274CF30A00AF477D /* ReminderDetailsView.swift in Sources */,
88C30CD6274D1B4500E6694D /* ReminderDetailsViewModel.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// View+ConfirmationDialog.swift
// MakeItSo (iOS)
//
// Created by Peter Friese on 23.11.21.
// Copyright © 2021 Google LLC. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import SwiftUI

struct ConfirmationDialog: ViewModifier {
@Environment(\.dismiss) private var dismiss
@State private var presentingConfirmationDialog: Bool = false

var isModified: Bool
var onCancel: (() -> Void)?
var onCommit: () -> Void

private func doCancel() {
onCancel?()
dismiss()
}

private func doCommit() {
onCommit()
dismiss()
}

func body(content: Content) -> some View {
NavigationView {
content
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", role: .cancel) {
if isModified {
presentingConfirmationDialog.toggle()
}
else {
doCancel()
}
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: doCommit)
}
}
.confirmationDialog("", isPresented: $presentingConfirmationDialog) {
Button("Discard Changes", role: .destructive, action: doCancel)
Button("Cancel", role: .cancel, action: { })
}
}
// Option 1: use a closure to handle the attempt to dismiss
// .interactiveDismissDisabled(isModified) {
// presentingConfirmationDialog.toggle()
// }
// Option 2: bind attempt to dismiss to a boolean state variable that drives the UI
.interactiveDismissDisabled(isModified, attemptToDismiss: $presentingConfirmationDialog)
}
}

extension View {
func confirmationDialog(isModified: Bool, onCancel: (() -> Void)? = nil, onCommit: @escaping () -> Void) -> some View {
self.modifier(ConfirmationDialog(isModified: isModified, onCancel: onCancel, onCommit: onCommit))
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//
// View+InteractiveDismissDisable.swift
// MakeItSo (iOS)
//
// Created by Peter Friese on 25.11.21.
// Copyright © 2021 Google LLC. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import SwiftUI

extension View {
public func interactiveDismissDisabled(_ isDisabled: Bool = true, onAttemptToDismiss: (() -> Void)? = nil) -> some View {
InteractiveDismissableView(view: self, isDisabled: isDisabled, onAttemptToDismiss: onAttemptToDismiss)
}

public func interactiveDismissDisabled(_ isDisabled: Bool = true, attemptToDismiss: Binding<Bool>) -> some View {
InteractiveDismissableView(view: self, isDisabled: isDisabled) {
attemptToDismiss.wrappedValue.toggle()
}
}
}

private struct InteractiveDismissableView<T: View>: UIViewControllerRepresentable {
let view: T
let isDisabled: Bool
let onAttemptToDismiss: (() -> Void)?

func makeUIViewController(context: Context) -> UIHostingController<T> {
UIHostingController(rootView: view)
}

func updateUIViewController(_ uiViewController: UIHostingController<T>, context: Context) {
context.coordinator.dismissableView = self
uiViewController.rootView = view
uiViewController.parent?.presentationController?.delegate = context.coordinator
}

func makeCoordinator() -> Coordinator {
Coordinator(self)
}

class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate {
var dismissableView: InteractiveDismissableView

init(_ dismissableView: InteractiveDismissableView) {
self.dismissableView = dismissableView
}

func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
!dismissableView.isDisabled
}

func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
dismissableView.onAttemptToDismiss?()
}
}
}

struct ContentView: View {
@State var showingSheet = false
@State var name: String = "Johnny Appleseed"
var body: some View {
Form {
Section("User Profile") {
Text(name)
}
Button("Edit", action: { showingSheet.toggle() })
}
.sheet(isPresented: $showingSheet) {
EditView(name: $name)
}
}
}

private class ViewModel: ObservableObject {
@Published var name: String
private var original: String

var isModified: Bool {
print("\(name) - \(original)")
return name != original
}

init(name: String) {
self.name = name
self.original = name
}
}

private struct EditView: View {
@Environment(\.dismiss) var dismiss
@Binding var name: String

@StateObject private var viewModel: ViewModel
@State var showingConfirmationDialog = false

init(name: Binding<String>) {
self._name = name
self._viewModel = StateObject(wrappedValue: ViewModel(name: name.wrappedValue))
}

var body: some View {
NavigationView {
Form {
TextField("Enter your name", text: $viewModel.name)
}
.navigationTitle("Edit")
.navigationBarTitleDisplayMode(.inline)
}
.interactiveDismissDisabled(viewModel.isModified) {
showingConfirmationDialog.toggle()
}
.confirmationDialog("", isPresented: $showingConfirmationDialog) {
Button("Save") {
name = viewModel.name
dismiss()
}
Button("Discard", role: .destructive) {
dismiss()
}
Button("Cancel", role: .cancel) { }
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ import Combine

class ReminderDetailsViewModel: ObservableObject {
@Published var reminder: Reminder
private var original: Reminder

init(reminder: Reminder) {
self.reminder = reminder
original = reminder
}

var isModified: Bool {
original != reminder
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,51 +20,39 @@
import SwiftUI

struct ReminderDetailsView: View {
@Environment(\.dismiss) private var dismiss
@ObservedObject private var viewModel: ReminderDetailsViewModel

private var onCancel: (() -> Void)?
private var onCommit: (Reminder) -> Void

init(reminder: Reminder, onCommit: @escaping (Reminder) -> Void) {
init(reminder: Reminder, onCancel: (() -> Void)? = nil, onCommit: @escaping (Reminder) -> Void) {
self.viewModel = ReminderDetailsViewModel(reminder: reminder)
self.onCommit = onCommit
}

var body: some View {
NavigationView {
Form {
Section {
TextField("Title", text: $viewModel.reminder.title)
}
Section {
Toggle(isOn: $viewModel.reminder.flagged) {
HStack {
Image(systemName: "flag.fill")
.frame(width: 26, height: 26, alignment: .center)
.background(.orange)
.foregroundColor(.white)
.cornerRadius(4)
Text("Flag")
}
}
}
Form {
Section {
TextField("Title", text: $viewModel.reminder.title)
}
.navigationTitle("Details")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", role: .cancel) {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
onCommit(viewModel.reminder)
dismiss()
Section {
Toggle(isOn: $viewModel.reminder.flagged) {
HStack {
Image(systemName: "flag.fill")
.frame(width: 26, height: 26, alignment: .center)
.background(.orange)
.foregroundColor(.white)
.cornerRadius(4)
Text("Flag")
}
}
}
}
.navigationTitle("Details")
.navigationBarTitleDisplayMode(.inline)
.confirmationDialog(isModified: viewModel.isModified) {
onCommit(viewModel.reminder)
}
}
}

Expand Down

0 comments on commit ccc55d4

Please sign in to comment.