Created
November 4, 2021 12:17
-
-
Save disc0infern0/67da1a717e832d4d8b2d272b6986beb1 to your computer and use it in GitHub Desktop.
a Reminder List with control of focus, adding new values with double click, auto deleting nil values, using Combine for delay
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ReminderList.swift | |
// | |
// Demonstrates:- | |
// list settings (style, colouring etc ) | |
// autoscrolling | |
// control of focus in List, | |
// adding new values with enter, or double click, | |
// auto deleting nil values, | |
// using Combine for managed delay/UI updates | |
// | |
// Created by Andrew Cowley on 29/10/2021. | |
// | |
import SwiftUI | |
import Combine | |
struct ReminderList: View { | |
@EnvironmentObject | |
var vm : ReminderListVM | |
@FocusState | |
private var focusedField: RowID? | |
var body: some View { | |
ScrollViewReader { proxy in | |
List() { | |
ForEach($vm.reminders) { $reminder in | |
ReminderRow(reminder: $reminder) | |
.listRowBackground ( Color.blue ) | |
.listRowInsets(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10)) | |
.swipeActions() { | |
Button(role: .destructive) {vm.deleteReminder(reminder)} | |
label: { Label("Delete", systemImage: "trash" ) } | |
} | |
.focused($focusedField, equals: .row(id: reminder.id)) | |
.onSubmit { vm.submit(reminder) } | |
.onTapGesture { vm.tap(reminder: reminder) } | |
}.background(Color.teal) | |
} | |
.onChange(of: vm.reminders) {_ in proxy.scrollTo(vm.focusedID, anchor: .bottom) } //keep new entries visible | |
.listStyle(.plain) | |
.background(Color.indigo) | |
.animation(.easeInOut(duration: 0.6), value: vm.reminders ) | |
.sync($vm.focusedField, $focusedField) | |
// a tap on a valid row will first fire the background tap below, then the tap on the reminder | |
.gesture( TapGesture(count: 1) .onEnded( vm.tap ) ) | |
.highPriorityGesture( TapGesture(count: 2) .onEnded( vm.doubleTap ) ) | |
} | |
.background( Color.secondary ) | |
.environment(\.defaultMinListRowHeight, 10) | |
} // body | |
} | |
enum RowID : Hashable { case row(id: String) } | |
class ReminderListVM : ObservableObject{ | |
@Published var reminders: [Reminder] = Reminder.examples | |
@Published var focusedField: RowID? = nil | |
@Published var delayedFocusedField: RowID? = nil | |
private var cancellables = Set<AnyCancellable>() | |
var previousFocusedField: RowID? // intermediate value that adds delay | |
var focusedID: String { rowid(focusedField) } | |
// Helper function for debugging/printing FocusState fields | |
func rowid(_ row: RowID?) -> String { | |
guard case.row(let id) = row else { return "" } | |
return id | |
} | |
init() { | |
// push the values to focusedField after a small delay | |
$delayedFocusedField | |
.removeDuplicates() | |
.delay(for: 0.01, scheduler: RunLoop.current ) | |
.assign(to: &$focusedField) | |
// Remove last focused entry if it is empty | |
$focusedField | |
.removeDuplicates() | |
.receive(on: RunLoop.main) | |
.compactMap { focusedField -> Int? in | |
defer { self.previousFocusedField = focusedField } | |
guard | |
self.previousFocusedField != nil, | |
case .row(let previousId) = self.previousFocusedField, | |
let previousIndex = self.reminders.firstIndex(where: { $0.id == previousId } ), | |
self.reminders[previousIndex].text.isEmpty | |
else { return nil } | |
return previousIndex | |
} | |
.sink { self.reminders.remove(at: $0) } | |
.store(in: &cancellables) | |
} | |
func submit(_ reminder: Reminder) { | |
if reminder.text.count > 0 { | |
newReminder(after: reminder) | |
} else { // put cursor back on the reminder ( onSubmit cancels focus ) | |
setFocus(reminder) | |
} | |
} | |
/// for a tap anywhere on the List, including blank areas | |
func tap() { | |
// Important! Do not delay this action ,since we may need to override it | |
// on the reminder row. Setting the same delay causes indeterminate results | |
focusedField = nil | |
} | |
/// for a tap on a valid reminder list row | |
func tap(reminder: Reminder) { | |
focusedField = .row(id: reminder.id) // override background tap that sets nil focus | |
} | |
func doubleTap() { | |
if let lastReminder = reminders.last { | |
if lastReminder.text.count > 0 { | |
newReminder(after: lastReminder) | |
} | |
else { | |
focusedField = .row(id: lastReminder.id) | |
} | |
} | |
} | |
func newReminder(after reminder: Reminder) { | |
let newReminder = Reminder() | |
if let index = reminders.firstIndex(where: {$0.id == reminder.id }) { | |
reminders.insert(newReminder,at: index+1) | |
} else { | |
reminders.append(newReminder) | |
} | |
setFocus(newReminder) | |
} | |
func deleteReminder(_ reminder: Reminder) { | |
Just(reminder) | |
.delay(for: .seconds(0.25), scheduler: RunLoop.main) | |
.sink { reminder in | |
self.reminders.removeAll { $0.id == reminder.id } | |
} | |
.store(in: &cancellables) | |
} | |
func setFocus(_ reminder: Reminder) { | |
delayedFocusedField = .row(id: reminder.id) | |
} | |
} | |
struct Reminder: Identifiable, Equatable { | |
var id = UUID().uuidString | |
var text: String = "" | |
static var examples = [ Reminder(id: "one",text: "one"), Reminder(id: "two",text: "two"), Reminder(id: "three", text: "three")] | |
} | |
struct ReminderRow: View { | |
@Binding var reminder: Reminder | |
var body: some View { | |
HStack{ | |
TextField("Reminder name", text: $reminder.text ) | |
.accentColor(.primary) // cursor color | |
.disableAutocorrection(true) | |
} | |
} | |
} | |
extension View { | |
/// Mirror changes between an @Published variable (typically in your View Model) and an @FocusedState variable in a view | |
func sync<T: Equatable>(_ field1: Binding<T>, _ field2: FocusState<T>.Binding ) -> some View { | |
return self | |
.onChange(of: field1.wrappedValue) { field2.wrappedValue = $0 } | |
.onChange(of: field2.wrappedValue) { field1.wrappedValue = $0 } | |
} | |
} | |
struct ReminderList_Previews: PreviewProvider { | |
static var previews: some View { | |
ReminderList().environmentObject(ReminderListVM()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment