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:
2026-09-19 16:02:23 +02:00
co-authored by Claude Sonnet 5
parent 1f7ee90882
commit ed6e4ab9ab
4 changed files with 112 additions and 52 deletions
+102 -49
View File
@@ -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)
}
#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)
.onReceive(
NotificationCenter.default.publisher(for: sceneBackgroundNotification)
) { _ in
session.lock()
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(items[index])
@ViewBuilder
private var vaultView: some View {
let vaultVM = VaultViewModel(session: session)
#if os(macOS)
MacVaultView(vm: vaultVM)
#else
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
}
}
fileprivate struct NavigationViewWrapper<Content: View>: View {
let content: () -> Content
// 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 {
#if os(macOS)
NavigationSplitView {
content()
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: {
Text("Select an item")
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") }
}
}
#else
content()
#endif
}
}
#Preview {
ContentView()
.modelContainer(for: Item.self, inMemory: true)
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
+7 -1
View File
@@ -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>
+1 -1
View File
@@ -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
+1
View File
@@ -4,6 +4,7 @@
//
import SwiftUI
import UniformTypeIdentifiers
import MyPassCore
struct UnlockView: View {