Skip to content

Commit

Permalink
Merge branch 'feature/reminder-details-screen-duedate-priority' into …
Browse files Browse the repository at this point in the history
…develop
  • Loading branch information
peterfriese committed Dec 3, 2021
2 parents ccc55d4 + 9c4eff7 commit 91f33c8
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 21 deletions.
6 changes: 6 additions & 0 deletions code/frontend/MakeItSo/MakeItSo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
881EF5BD272DC399004761E5 /* View+Focus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881EF5BC272DC399004761E5 /* View+Focus.swift */; };
887B6FAD273ED4180028263D /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887B6FA9273ED4180028263D /* EmptyStateView.swift */; };
887B6FAE273ED4180028263D /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 887B6FA9273ED4180028263D /* EmptyStateView.swift */; };
88A1B7432756541400DB0494 /* ReminderListRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A1B7422756541400DB0494 /* ReminderListRowViewModel.swift */; };
88A1B7442756541400DB0494 /* ReminderListRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A1B7422756541400DB0494 /* ReminderListRowViewModel.swift */; };
88C30CD6274D1B4500E6694D /* ReminderDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C30CD5274D1B4500E6694D /* ReminderDetailsViewModel.swift */; };
88C30CD7274D1B4500E6694D /* ReminderDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88C30CD5274D1B4500E6694D /* ReminderDetailsViewModel.swift */; };
88E7B7BE274CF30A00AF477D /* ReminderDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E7B7BD274CF30A00AF477D /* ReminderDetailsView.swift */; };
Expand All @@ -33,6 +35,7 @@
/* Begin PBXFileReference section */
881EF5BC272DC399004761E5 /* View+Focus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Focus.swift"; sourceTree = "<group>"; };
887B6FA9273ED4180028263D /* EmptyStateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = "<group>"; };
88A1B7422756541400DB0494 /* ReminderListRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReminderListRowViewModel.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>"; };
Expand Down Expand Up @@ -171,6 +174,7 @@
children = (
88FEECF62728072D00ED368C /* RemindersListViewModel.swift */,
88C30CD5274D1B4500E6694D /* ReminderDetailsViewModel.swift */,
88A1B7422756541400DB0494 /* ReminderListRowViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
Expand Down Expand Up @@ -280,6 +284,7 @@
88FEECF02727FEFF00ED368C /* Reminder.swift in Sources */,
88FA998D274D63A400670474 /* View+ConfirmationDialog.swift in Sources */,
88FEECFA27280F3D00ED368C /* ReminderListRowView.swift in Sources */,
88A1B7432756541400DB0494 /* ReminderListRowViewModel.swift in Sources */,
88FA99922750090200670474 /* View+InteractiveDismissDisable.swift in Sources */,
887B6FAD273ED4180028263D /* EmptyStateView.swift in Sources */,
88E7B7BE274CF30A00AF477D /* ReminderDetailsView.swift in Sources */,
Expand All @@ -301,6 +306,7 @@
88FEECDB27275ABD00ED368C /* MakeItSoApp.swift in Sources */,
88FEECF42728044100ED368C /* RemindersListView.swift in Sources */,
88FEECF82728072D00ED368C /* RemindersListViewModel.swift in Sources */,
88A1B7442756541400DB0494 /* ReminderListRowViewModel.swift in Sources */,
887B6FAE273ED4180028263D /* EmptyStateView.swift in Sources */,
88E7B7BF274CF30A00AF477D /* ReminderDetailsView.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import Foundation

enum Priority {
enum Priority: String {
case none
case low
case medium
Expand All @@ -37,10 +37,10 @@ struct Reminder {
var id: String = UUID().uuidString
var title: String
var notes: String?
var url: URL?
var url: String?

var dueDate: Date?
var dueTime: Date?
var hasDueTime: Bool = false

var tags: [Tag]?

Expand All @@ -61,7 +61,27 @@ struct Reminder {
var order: Int = 0
}

extension Priority: Codable, Equatable {
extension Priority: Codable, Equatable, Identifiable {
var id: Priority { self }
}

extension Priority: Comparable {
static func < (lhs: Priority, rhs: Priority) -> Bool {
guard let l = lhs.index, let r = rhs.index else { return false }
return l < r
}
}

// Conforming Priority to CaseIterable allows us to use it inside a `Picker` view
extension Priority: CaseIterable { }

// This allows us to determine the index of a case inside an enum.
// For example, this is used to compute the representation of a
// task priority (!, !!, !!!, or en empty string for "no priority").
extension CaseIterable where Self: Equatable {
var index: Self.AllCases.Index? {
return Self.allCases.firstIndex { self == $0 }
}
}

extension Tag: Codable, Equatable {
Expand All @@ -82,9 +102,9 @@ extension Reminder: Hashable {

extension Reminder {
static let samples = [
Reminder(title: "Build sample app"),
Reminder(title: "Tweet about surprising findings", flagged: true),
Reminder(title: "Write newsletter"),
Reminder(title: "Build sample app", dueDate: Date.now, priority: .high),
Reminder(title: "Tweet about surprising findings", dueDate: Date.now, flagged: true),
Reminder(title: "Write newsletter", priority: .medium),
Reminder(title: "Run YouTube video series"),
Reminder(title: "???"),
// Reminder(title: "PROFIT!!")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,56 @@
import Foundation
import Combine

extension Date {
func formattedRelativeToday() -> String {
if Calendar.autoupdatingCurrent.isDateInToday(self)
|| Calendar.autoupdatingCurrent.isDateInYesterday(self)
|| Calendar.autoupdatingCurrent.isDateInTomorrow(self) {

let formatStyle = Date.RelativeFormatStyle(
presentation: .named,
unitsStyle: .wide,
capitalizationContext: .beginningOfSentence)

return self.formatted(formatStyle)
}
else {
return self.formatted(date: .complete, time: .omitted)
}
}

func nearestHour() -> Date? {
var components = NSCalendar.current.dateComponents([.minute], from: self)
let minute = components.minute ?? 0
components.minute = minute >= 30 ? 60 - minute : -minute
return Calendar.current.date(byAdding: components, to: self)
}

func nextHour(basedOn date: Date? = nil) -> Date? {
let other = date ?? self

var timeComponents = Calendar.current.dateComponents([.hour, .minute], from: other)
let minute = timeComponents.minute ?? 0
timeComponents.minute = minute >= 0 ? 60 : 0

let dateComponents = Calendar.current.dateComponents([.year, .month, .day], from: self)

let newDateComponents = DateComponents(calendar: Calendar.current,
year: dateComponents.year,
month: dateComponents.month,
day: dateComponents.day,
hour: timeComponents.hour,
minute: timeComponents.minute)

return Calendar.current.date(from: newDateComponents)
}

func startOfDay() -> Date {
Calendar.current.startOfDay(for: self)
}
}


class ReminderDetailsViewModel: ObservableObject {
@Published var reminder: Reminder
private var original: Reminder
Expand All @@ -32,4 +82,93 @@ class ReminderDetailsViewModel: ObservableObject {
var isModified: Bool {
original != reminder
}

// The `notes` attribute on the Reminder model is optional.
// Using this approach allows us to bind the notes to SwiftUI
// views.
var notes: String {
get {
reminder.notes ?? ""
}
set {
reminder.notes = newValue
}
}

var url: String {
get {
reminder.url ?? ""
}
set {
reminder.url = newValue
}
}

var dueDate: Date {
get {
return reminder.dueDate ?? Date()
}
set {
reminder.dueDate = newValue
}
}

var dueTime: Date {
get {
return dueDate
}
set {
dueDate = newValue
}
}

var hasDueDate: Bool {
get {
reminder.dueDate != nil
}
set {
if newValue == true {
reminder.dueDate = Date()
isShowingDatePicker = true
}
else {
hasDueTime = false
reminder.dueDate = nil
isShowingDatePicker = false
}
}
}

var hasDueTime: Bool {
get {
reminder.hasDueTime
}
set {
if newValue == true {
guard let nearestHour = dueDate.nextHour(basedOn: Date()) else { return }
dueDate = nearestHour
reminder.hasDueTime = true
isShowingTimePicker = true
}
else {
dueDate = dueDate.startOfDay()
reminder.hasDueTime = false
isShowingTimePicker = false
}
}
}

@Published var isShowingDatePicker: Bool = false
@Published var isShowingTimePicker: Bool = false

func toggleTimePicker() {
isShowingTimePicker.toggle()
isShowingDatePicker = false
}

func toggleDatePicker() {
isShowingDatePicker.toggle()
isShowingTimePicker = false
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// ReminderListRowViewModel.swift
// MakeItSo
//
// Created by Peter Friese on 30.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
//
// http: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

class ReminderListRowViewModel: ObservableObject {
@Binding var reminder: Reminder

init(reminder: Binding<Reminder>) {
self._reminder = reminder
}

var priorityAdornment: String {
String(repeating: "!", count: (reminder.priority.index ?? 0))
}

var hasDueDate: Bool {
reminder.dueDate != nil
}

var hasDueTime: Bool {
reminder.hasDueTime
}

var dueDate: Date {
reminder.dueDate ?? Date()
}

var formattedDueDate: String {
guard let dueDate = reminder.dueDate else { return "" }
return dueDate.formattedRelativeToday()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,63 @@ struct ReminderDetailsView: View {
Form {
Section {
TextField("Title", text: $viewModel.reminder.title)
TextField("Notes", text: $viewModel.notes)
TextField("URL", text: $viewModel.url)
}
Section {
Toggle(isOn: $viewModel.hasDueDate) {
HStack {
Image(systemName: "calendar")
.frame(width: 26, height: 26, alignment: .center)
.background(.red)
.foregroundColor(.white)
.cornerRadius(4)
VStack(alignment: .leading) {
Text("Date")
if viewModel.hasDueDate {
Text(viewModel.dueDate.formattedRelativeToday())
.font(.caption2)
.foregroundColor(.blue)
} }
}
}
.contentShape(Rectangle())
.onTapGesture {
withAnimation {
viewModel.toggleDatePicker()
}
}
if viewModel.hasDueDate && viewModel.isShowingDatePicker {
DatePicker("Date", selection: $viewModel.dueDate, displayedComponents: .date)
.datePickerStyle(.graphical)
}

Toggle(isOn: $viewModel.hasDueTime) {
HStack {
Image(systemName: "clock")
.frame(width: 26, height: 26, alignment: .center)
.background(.blue)
.foregroundColor(.white)
.cornerRadius(4)
VStack(alignment: .leading) {
Text("Time")
if (viewModel.hasDueTime) {
Text(viewModel.dueTime, style: .time)
.font(.caption2)
.foregroundColor(.blue)
} }
}
}
.contentShape(Rectangle())
.onTapGesture {
withAnimation {
viewModel.toggleTimePicker()
}
}
if viewModel.hasDueTime && viewModel.isShowingTimePicker {
DatePicker("Date", selection: $viewModel.dueTime, displayedComponents: .hourAndMinute)
.datePickerStyle(.wheel)
}
}
Section {
Toggle(isOn: $viewModel.reminder.flagged) {
Expand All @@ -47,7 +104,15 @@ struct ReminderDetailsView: View {
}
}
}
Section {
Picker("Priority", selection: $viewModel.reminder.priority) {
ForEach(Priority.allCases) { prio in
Text(prio.rawValue.capitalized)
}
}
}
}
.animation(.default, value: viewModel.reminder)
.navigationTitle("Details")
.navigationBarTitleDisplayMode(.inline)
.confirmationDialog(isModified: viewModel.isModified) {
Expand Down
Loading

0 comments on commit 91f33c8

Please sign in to comment.