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 SwiftUI
|
||||||
import SwiftData
|
import MyPassCore
|
||||||
|
#if os(macOS)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@Environment(\.modelContext) private var modelContext
|
@StateObject private var session = VaultSession()
|
||||||
@Query private var items: [Item]
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationViewWrapper {
|
SwiftUI.Group {
|
||||||
List {
|
if session.isLocked {
|
||||||
ForEach(items) { item in
|
UnlockView(vm: UnlockViewModel(session: session))
|
||||||
NavigationLink {
|
} else {
|
||||||
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
|
vaultView
|
||||||
} label: {
|
|
||||||
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.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)
|
#if os(macOS)
|
||||||
.navigationSplitViewColumnWidth(min: 180, ideal: 200)
|
MacVaultView(vm: vaultVM)
|
||||||
#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")
|
|
||||||
}
|
|
||||||
#else
|
#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
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
// MARK: - macOS 3-column layout
|
||||||
ContentView()
|
|
||||||
.modelContainer(for: Item.self, inMemory: true)
|
#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">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.application-groups</key>
|
<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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ final class EntryEditViewModel: ObservableObject {
|
|||||||
|
|
||||||
var isEditing: Bool { existingEntry != nil }
|
var isEditing: Bool { existingEntry != nil }
|
||||||
|
|
||||||
func save(dismiss: () -> Void) {
|
func save(dismiss: @escaping () -> Void) {
|
||||||
Task {
|
Task {
|
||||||
isSaving = true
|
isSaving = true
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
import MyPassCore
|
import MyPassCore
|
||||||
|
|
||||||
struct UnlockView: View {
|
struct UnlockView: View {
|
||||||
|
|||||||
Reference in New Issue
Block a user