feat: wire ContentView router, macOS 3-column layout, scene lifecycle lock
Completes Phase 4 (Task 17) -- ContentView now routes between UnlockView
and the vault UI (NavigationStack on iOS, 3-column NavigationSplitView on
macOS) instead of the leftover SwiftData template, finally resolving the
long-standing "cannot find type Item" build break. Vault locks on
scene-background (iOS) / resign-active (macOS).
Fixes two more real bugs surfaced now that everything actually links
together: EntryEditViewModel.save's dismiss closure needed @escaping
(used inside Task {}), and UnlockView's UTType(filenameExtension:)
needed `import UniformTypeIdentifiers`.
Also restores MyPass.entitlements, which had regressed to an empty
App Group array with Keychain Sharing missing entirely since the
"wire up AutoFill extension target" commit -- likely clobbered by
Xcode reconciling capabilities at some point without a rebuild in
between to catch it. Verified clean `xcodebuild` on both iOS Simulator
and macOS destinations, and `swift test` still passes all 18 tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf
This commit is contained in:
+111
-58
@@ -6,75 +6,128 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import MyPassCore
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query private var items: [Item]
|
||||
@StateObject private var session = VaultSession()
|
||||
|
||||
var body: some View {
|
||||
NavigationViewWrapper {
|
||||
List {
|
||||
ForEach(items) { item in
|
||||
NavigationLink {
|
||||
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
|
||||
} label: {
|
||||
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
|
||||
SwiftUI.Group {
|
||||
if session.isLocked {
|
||||
UnlockView(vm: UnlockViewModel(session: session))
|
||||
} else {
|
||||
vaultView
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteItems)
|
||||
.onReceive(
|
||||
NotificationCenter.default.publisher(for: sceneBackgroundNotification)
|
||||
) { _ in
|
||||
session.lock()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var vaultView: some View {
|
||||
let vaultVM = VaultViewModel(session: session)
|
||||
#if os(macOS)
|
||||
.navigationSplitViewColumnWidth(min: 180, ideal: 200)
|
||||
#endif
|
||||
.toolbar {
|
||||
#if os(iOS)
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
EditButton()
|
||||
}
|
||||
#endif
|
||||
ToolbarItem {
|
||||
Button(action: addItem) {
|
||||
Label("Add Item", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addItem() {
|
||||
withAnimation {
|
||||
let newItem = Item(timestamp: Date())
|
||||
modelContext.insert(newItem)
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteItems(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
for index in offsets {
|
||||
modelContext.delete(items[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct NavigationViewWrapper<Content: View>: View {
|
||||
let content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
NavigationSplitView {
|
||||
content()
|
||||
} detail: {
|
||||
Text("Select an item")
|
||||
}
|
||||
MacVaultView(vm: vaultVM)
|
||||
#else
|
||||
content()
|
||||
NavigationStack {
|
||||
if let root = session.database?.root {
|
||||
GroupBrowserView(vm: vaultVM, group: root)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private var sceneBackgroundNotification: Notification.Name {
|
||||
#if os(iOS)
|
||||
UIScene.didEnterBackgroundNotification
|
||||
#else
|
||||
NSApplication.didResignActiveNotification
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
.modelContainer(for: Item.self, inMemory: true)
|
||||
// MARK: - macOS 3-column layout
|
||||
|
||||
#if os(macOS)
|
||||
private struct MacVaultView: View {
|
||||
@ObservedObject var vm: VaultViewModel
|
||||
@State private var selectedGroup: MyPassCore.Group?
|
||||
@State private var selectedEntry: Entry?
|
||||
@State private var showAddEntry = false
|
||||
|
||||
private var activeGroup: MyPassCore.Group? { selectedGroup ?? vm.session.database?.root }
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
GroupSidebarView(vm: vm, selectedGroup: $selectedGroup)
|
||||
} content: {
|
||||
if vm.isSearching {
|
||||
SearchView(vm: vm, selectedEntry: $selectedEntry)
|
||||
} else if let group = activeGroup {
|
||||
entryList(for: group)
|
||||
} else {
|
||||
Text("Select a group").foregroundStyle(.secondary)
|
||||
}
|
||||
} detail: {
|
||||
if let entry = selectedEntry {
|
||||
EntryDetailView(entry: entry, session: vm.session)
|
||||
} else {
|
||||
Text("Select an entry").foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.searchable(text: $vm.searchQuery, prompt: "Search all entries…")
|
||||
.sheet(isPresented: $showAddEntry) {
|
||||
if let group = activeGroup {
|
||||
EntryEditView(vm: EntryEditViewModel(session: vm.session, groupId: group.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func entryList(for group: MyPassCore.Group) -> some View {
|
||||
List(group.entries, selection: $selectedEntry) { entry in
|
||||
EntryListRow(entry: entry).tag(entry)
|
||||
}
|
||||
.navigationTitle(group.name)
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
Button { showAddEntry = true } label: { Image(systemName: "plus") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct GroupSidebarView: View {
|
||||
@ObservedObject var vm: VaultViewModel
|
||||
@Binding var selectedGroup: MyPassCore.Group?
|
||||
|
||||
var body: some View {
|
||||
List(selection: $selectedGroup) {
|
||||
if let root = vm.session.database?.root {
|
||||
GroupNode(group: root)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Groups")
|
||||
}
|
||||
}
|
||||
|
||||
private struct GroupNode: View {
|
||||
let group: MyPassCore.Group
|
||||
var body: some View {
|
||||
if group.subgroups.isEmpty {
|
||||
Label(group.name, systemImage: "folder").tag(group)
|
||||
} else {
|
||||
DisclosureGroup {
|
||||
ForEach(group.subgroups) { sub in GroupNode(group: sub) }
|
||||
} label: {
|
||||
Label(group.name, systemImage: "folder").tag(group)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array/>
|
||||
<array>
|
||||
<string>group.org.antiloop222.mypass</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)org.antiloop222.mypass</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -39,7 +39,7 @@ final class EntryEditViewModel: ObservableObject {
|
||||
|
||||
var isEditing: Bool { existingEntry != nil }
|
||||
|
||||
func save(dismiss: () -> Void) {
|
||||
func save(dismiss: @escaping () -> Void) {
|
||||
Task {
|
||||
isSaving = true
|
||||
errorMessage = nil
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import MyPassCore
|
||||
|
||||
struct UnlockView: View {
|
||||
|
||||
Reference in New Issue
Block a user