Parcourir la source

Design macOS Notes-style UI shell.

Embed a SwiftUI NavigationSplitView (sidebar, search/list, editor) inside the existing storyboard-based ViewController and add gitignore for Xcode build artifacts.

Made-with: Cursor
huzaifahayat12 il y a 4 mois
Parent
commit
a6aef9112b
3 fichiers modifiés avec 231 ajouts et 1 suppressions
  1. 6 0
      .gitignore
  2. 209 0
      notes_app/NotesAppRootView.swift
  3. 16 1
      notes_app/ViewController.swift

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+DerivedData/
+*.xcuserstate
+*.xcworkspace/
+xcuserdata/
+xcodebuild.log
+.DS_Store

+ 209 - 0
notes_app/NotesAppRootView.swift

@@ -0,0 +1,209 @@
+import SwiftUI
+import AppKit
+
+private enum Provider: String, CaseIterable, Identifiable {
+    case iCloud = "iCloud"
+    case google = "Google"
+
+    var id: String { rawValue }
+}
+
+private struct NoteCollection: Identifiable, Hashable {
+    let id: UUID
+    let provider: Provider
+    let name: String
+}
+
+private struct Note: Identifiable, Equatable, Hashable {
+    let id: UUID
+    var title: String
+    var body: String
+    var updatedAt: Date
+}
+
+struct NotesAppRootView: View {
+    @State private var searchText: String = ""
+    private static let defaultCollectionID: UUID = UUID(uuidString: "2E6A2D4E-0B3D-4E8A-A5B7-8D9E0C9E2A01")!
+    @State private var selectedCollectionID: UUID? = NotesAppRootView.defaultCollectionID
+    @State private var selectedNoteID: UUID? = nil
+
+    // Keep this local for now: the goal is to design/match the UI.
+    @State private var notes: [Note] = []
+
+    private var collections: [NoteCollection] = [
+        NoteCollection(id: NotesAppRootView.defaultCollectionID, provider: .iCloud, name: "Notes")
+    ]
+
+    private var selectedCollection: NoteCollection? {
+        collections.first { $0.id == selectedCollectionID }
+    }
+
+    private var filteredNotes: [Note] {
+        // Since this is a UI-first pass, we treat notes as belonging to the iCloud "Notes" collection only.
+        guard let selectedCollection else { return [] }
+
+        // Currently notes are empty by default to match your screenshot.
+        let scoped = (selectedCollection.provider == .iCloud) ? notes : []
+
+        let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+        if trimmed.isEmpty { return scoped }
+
+        return scoped.filter {
+            $0.title.lowercased().contains(trimmed) || $0.body.lowercased().contains(trimmed)
+        }
+    }
+
+    var body: some View {
+        NavigationSplitView {
+            SidebarView(
+                collections: collections,
+                notes: notes,
+                selectedCollectionID: $selectedCollectionID
+            )
+        } content: {
+            VStack(spacing: 0) {
+                SearchBar(text: $searchText)
+
+                Group {
+                    if filteredNotes.isEmpty {
+                        EmptyListView(text: "No Notes")
+                    } else {
+                        NotesListView(
+                            notes: filteredNotes,
+                            selectedNoteID: $selectedNoteID
+                        )
+                    }
+                }
+                .frame(maxWidth: .infinity, maxHeight: .infinity)
+            }
+            .background(Color(nsColor: .textBackgroundColor))
+        } detail: {
+            NoteEditorView(
+                notes: $notes,
+                selectedNoteID: selectedNoteID
+            )
+            .background(Color(nsColor: .textBackgroundColor))
+        }
+        .preferredColorScheme(.dark)
+    }
+}
+
+private struct SidebarView: View {
+    let collections: [NoteCollection]
+    let notes: [Note]
+
+    @Binding var selectedCollectionID: UUID?
+
+    private func notesCount(for collection: NoteCollection) -> Int {
+        if collection.provider != .iCloud { return 0 }
+        return notes.count
+    }
+
+    var body: some View {
+        List(selection: $selectedCollectionID) {
+            ForEach(Provider.allCases) { provider in
+                Section(header: Text(provider == .iCloud ? "iCloud" : "Google")) {
+                    ForEach(collections.filter { $0.provider == provider }) { collection in
+                        HStack(spacing: 8) {
+                            Text(collection.name)
+                                .foregroundStyle(.primary)
+                            Spacer(minLength: 8)
+                            Text("\(notesCount(for: collection))")
+                                .foregroundStyle(.secondary)
+                                .font(.system(size: 12, weight: .semibold))
+                        }
+                        .tag(collection.id)
+                    }
+                }
+            }
+        }
+        .listStyle(.sidebar)
+        .frame(minWidth: 210)
+    }
+}
+
+private struct SearchBar: View {
+    @Binding var text: String
+
+    var body: some View {
+        HStack(spacing: 8) {
+            Image(systemName: "magnifyingglass")
+                .foregroundStyle(.secondary)
+            TextField("Search", text: $text)
+                .textFieldStyle(.plain)
+                .foregroundStyle(.primary)
+        }
+        .padding(10)
+        .background(Color(nsColor: .windowBackgroundColor))
+        .overlay(
+            RoundedRectangle(cornerRadius: 10)
+                .stroke(Color(nsColor: .separatorColor).opacity(0.35), lineWidth: 1)
+        )
+        .padding(12)
+    }
+}
+
+private struct EmptyListView: View {
+    let text: String
+
+    var body: some View {
+        VStack {
+            Spacer()
+            Text(text)
+                .foregroundStyle(.secondary)
+                .font(.system(size: 16, weight: .semibold))
+            Spacer()
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity)
+    }
+}
+
+private struct NotesListView: View {
+    let notes: [Note]
+    @Binding var selectedNoteID: UUID?
+
+    var body: some View {
+        List(selection: $selectedNoteID) {
+            ForEach(notes) { note in
+                HStack(spacing: 8) {
+                    Text(note.title)
+                        .lineLimit(1)
+                    Spacer(minLength: 8)
+                }
+                .tag(note.id)
+            }
+        }
+        .listStyle(.inset)
+    }
+}
+
+private struct NoteEditorView: View {
+    @Binding var notes: [Note]
+    let selectedNoteID: UUID?
+
+    var body: some View {
+        VStack(spacing: 10) {
+            if let selectedNoteID,
+               let index = notes.firstIndex(where: { $0.id == selectedNoteID }) {
+                TextField("Title", text: $notes[index].title)
+                    .textFieldStyle(.plain)
+                    .font(.system(size: 22, weight: .semibold))
+                    .padding(.horizontal, 14)
+
+                Divider()
+                    .padding(.horizontal, 14)
+
+                TextEditor(text: $notes[index].body)
+                    .font(.system(size: 14))
+                    .foregroundStyle(.primary)
+                    .padding(14)
+                    .background(Color(nsColor: .textBackgroundColor))
+            } else {
+                // Keep the editor empty to match the screenshot's "nothing selected" state.
+                Spacer()
+            }
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
+    }
+}
+

+ 16 - 1
notes_app/ViewController.swift

@@ -6,13 +6,28 @@
 //
 
 import Cocoa
+import SwiftUI
 
 class ViewController: NSViewController {
 
+    private var hostingView: NSHostingView<NotesAppRootView>?
+
     override func viewDidLoad() {
         super.viewDidLoad()
 
-        // Do any additional setup after loading the view.
+        let rootView = NotesAppRootView()
+        let hostingView = NSHostingView(rootView: rootView)
+        hostingView.translatesAutoresizingMaskIntoConstraints = false
+
+        view.addSubview(hostingView)
+        NSLayoutConstraint.activate([
+            hostingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+            hostingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+            hostingView.topAnchor.constraint(equalTo: view.topAnchor),
+            hostingView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+        ])
+
+        self.hostingView = hostingView
     }
 
     override var representedObject: Any? {