Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Created June 30, 2020 12:27
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chriseidhof/a09473fcef9ac3f2af820eab14b951b6 to your computer and use it in GitHub Desktop.
Save chriseidhof/a09473fcef9ac3f2af820eab14b951b6 to your computer and use it in GitHub Desktop.
//
// ContentView.swift
// NotesClone
//
// Created by Chris Eidhof on 29.06.20.
//
import SwiftUI
@main
struct NotesCloneApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct Note: Codable, Identifiable, Hashable {
private(set) var id = UUID()
var createdAt = Date()
var title: String
var text: String
}
let encoder = JSONEncoder()
let decoder = JSONDecoder()
final class JSONStore<A>: ObservableObject {
@Published var value: A {
didSet {
// print("Did set: \(value)")
}
}
init(_ initialValue: A) {
self.value = initialValue
}
}
extension Data {
var notes: [UUID: Note] {
get {
return (try? decoder.decode([UUID: Note].self, from: self)) ?? [:]
}
set {
self = (try? encoder.encode(newValue)) ?? .init()
}
}
}
struct Folder: Identifiable, Codable, Hashable {
var id = UUID()
var name = "On My Mac"
var notes: [UUID: Note]
}
func sample() -> [Folder] {
let note = Note(title: "My Note", text: "My first note.")
let f = Folder(notes: [note.id: note])
return [f]
}
struct ContentView: View {
@ObservedObject var store: JSONStore<[Folder]> = JSONStore(sample())
@State var selectedIndex: Set<Int> = [0]
var body: some View {
NavigationView {
VStack(alignment: .leading) {
List(selection: $selectedIndex) {
ForEach(store.value) { folder in
NavigationLink(
destination: FolderView(store: store, folder: folder),
label: {
Label(folder.name, systemImage: "folder")
}
).tag(folder.id)
}
}.listStyle(SidebarListStyle())
Button(action: {
store.value.append(Folder(name: "New Folder", notes: [:]))
}) {
HStack {
Image(systemName: "plus.circle.fill")
Text("New Folder")
}.padding()
}.buttonStyle(PlainButtonStyle())
}
.frame(minWidth: 100, maxWidth: 200, maxHeight: .infinity)
Text("Select a note").frame(maxWidth: .infinity, maxHeight: .infinity)
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
extension Dictionary {
subscript(unsafe key: Key) -> Value {
get { self[key]! }
set { self[key] = newValue }
}
}
extension Dictionary where Value == Note {
var sortedKeys: [Key] {
return self.sorted(by: { $0.value.createdAt > $1.value.createdAt }).map { $0.key }
}
}
enum ListOrGrid: Hashable {
case list
case grid
}
struct FolderView: View {
@ObservedObject var store: JSONStore<[Folder]>
var folder: Folder
var notes: [UUID:Note] { folder.notes }
@State var state: ListOrGrid = .list
var folderBinding: Binding<Folder> {
let folderIx = store.value.firstIndex(where: { $0.id == folder.id })!
return $store.value[folderIx]
}
func noteBinding(id: UUID) -> Binding<Note> {
folderBinding.notes[unsafe: id]
}
func noteLink(id: UUID) -> some View {
let dest: NoteView = NoteView(note: noteBinding(id: id))
return NavigationLink(
destination: dest,
label: {
VStack(alignment: .leading) {
let note = notes[unsafe: id]
Text(note.title).font(.headline)
HStack {
Text(note.createdAt, style: .date).layoutPriority(10)
Text(note.text).lineLimit(1)
}
}
.padding(.vertical, 8)
})
}
func gridCell(id: UUID) -> some View {
print(selectedNote == id)
let rect = RoundedRectangle(cornerRadius: 10)
let cell = rect
.fill(BackgroundStyle())
.shadow(color: Color.gray.opacity(0.5), radius: 5)
.overlay(rect.fill(selectedNote == id ? Color.yellow : Color.clear))
.overlay(
Text(notes[unsafe: id].text)
.font(Font.caption2)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
)
.onTapGesture(count: 1) {
print("Selecting \(id)")
}
.onTapGesture(count: 2) {
print("Openiing \(id)")
self.selectedNote = id
}
return
cell.frame(width: 200, height: 150)
}
@State var selectedNote: UUID? = nil
@State var selectedNotes: Set<UUID> = []
var body: some View {
Group {
switch state {
case .list:
NavigationView {
List(selection: $selectedNotes) {
ForEach(Array(notes.sortedKeys), id: \.self) { (id: UUID) in
noteLink(id: id).tag(id)
}.onDelete { indices in
let keys = notes.sortedKeys
let ids = indices.map { keys[$0] }
for i in ids {
// self.store.delete(note) = nil
print("TODO delete \(i)")
}
}
}.listStyle(InsetListStyle())
Text("TODO").frame(maxWidth: .infinity, maxHeight: .infinity)
}
case .grid:
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200))], alignment: .leading) {
ForEach(Array(notes.sortedKeys), id: \.self) { id in
gridCell(id: id)
.frame(width: 200, height: 150)
}
}.padding()
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.navigationTitle(folder.name)
.toolbar {
ToolbarItem(placement: .navigation) {
Picker(selection: $state, label: EmptyView()) {
Image(systemName: "list.bullet").padding(4).tag(ListOrGrid.list)
Image(systemName: "square.grid.2x2").padding(4).tag(ListOrGrid.grid)
// Label("List", systemImage: "list.bullet").tag(ListOrGrid.list)
// Label("Grid", systemImage: "square.grid.2x2").tag(ListOrGrid.grid)
}
.labelStyle(IconOnlyLabelStyle())
.pickerStyle(SegmentedPickerStyle())
}
ToolbarItem(placement: .primaryAction) {
Button(action: {
let newNote = Note(title: "New Note", text: "New Note \(Date())")
withAnimation(.default) {
self.folderBinding.notes[newNote.id].wrappedValue = newNote
print(self.folderBinding.notes.wrappedValue.count)
}
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
}
struct NoteView: View {
@Binding var note: Note
var body: some View {
TextEditor(text: $note.text)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
.navigationTitle(note.title)
.toolbar(items: {
ToolbarItem(placement: .primaryAction) {
Button {
} label: {
// todo the image doesn't show up? the button's there but blank.
Label("Share", systemImage: "square.and.arrow.up")
.labelStyle(IconOnlyLabelStyle())
}
}
})
.presentedWindowToolbarStyle(UnifiedCompactWindowToolbarStyle())
.presentedWindowStyle(HiddenTitleBarWindowStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment