feat(ios): ship iOS SwiftUI client (VoiceCatiOS)
Full SwiftUI app at clients/apple/iOS/VoiceCatiOS.xcodeproj:
- 24 Swift source files: AppState + SessionState (@Observable @MainActor),
AudioSessionManager (AVAudioSession owner + interruption/route handling),
ServerListStore/SavedServer (App Group container + Keychain sharing),
and 14 SwiftUI views covering the full feature set
- NavigationSplitView on iPad, TabView on iPhone (horizontalSizeClass)
- Channel tree via OutlineGroup, user list with context menu admin actions
- PTT via DragGesture(minimumDistance: 0) + @GestureState
- onEvent closures hop to MainActor via Task { @MainActor in ... }
- App Group: group.cat.voice.VoiceCat (shared with future ReplayKit extension)
C ABI: add vc_audio_suspend / vc_audio_resume (AudioEngine::suspend/resume)
called by AudioSessionManager on AVAudioSession interruption events.
XCFramework: add ios-arm64 and ios-arm64-simulator slices to build-xcframework.sh;
Package.swift gains .iOS(.v17) platform; CMakePresets.json adds apple-ios /
apple-ios-sim presets with arm64-ios / arm64-ios-simulator vcpkg triplets.
Verified: xcodebuild -target VoiceCatiOS -sdk iphonesimulator26.5 BUILD SUCCEEDED.
This commit is contained in:
153
clients/apple/iOS/VoiceCatiOS/Views/AccountsView.swift
Normal file
153
clients/apple/iOS/VoiceCatiOS/Views/AccountsView.swift
Normal file
@@ -0,0 +1,153 @@
|
||||
import SwiftUI
|
||||
import VoiceCatCore
|
||||
|
||||
struct AccountsView: View {
|
||||
@Bindable var session: SessionState
|
||||
@State private var showCreateAccount = false
|
||||
@State private var newUsername = ""
|
||||
@State private var newPassword = ""
|
||||
@State private var accountToDelete: Account?
|
||||
@State private var showResetPassword = false
|
||||
@State private var resetForAccount: Account?
|
||||
@State private var resetPassword = ""
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(session.accounts, id: \.username) { account in
|
||||
AccountRowView(account: account)
|
||||
.swipeActions(edge: .trailing) {
|
||||
Button(role: .destructive) {
|
||||
accountToDelete = account
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
Button {
|
||||
resetForAccount = account
|
||||
showResetPassword = true
|
||||
} label: {
|
||||
Label("Reset PW", systemImage: "key")
|
||||
}
|
||||
.tint(.orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Accounts")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
showCreateAccount = true
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
.accessibilityLabel("Create account")
|
||||
}
|
||||
}
|
||||
.refreshable {
|
||||
session.fetchAccountList()
|
||||
}
|
||||
.onAppear {
|
||||
session.fetchAccountList()
|
||||
}
|
||||
.confirmationDialog("Delete account?", isPresented: Binding(
|
||||
get: { accountToDelete != nil },
|
||||
set: { if !$0 { accountToDelete = nil } }
|
||||
)) {
|
||||
if let a = accountToDelete {
|
||||
Button("Delete \(a.username)", role: .destructive) {
|
||||
session.deleteAccount(username: a.username)
|
||||
accountToDelete = nil
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) { accountToDelete = nil }
|
||||
}
|
||||
.sheet(isPresented: $showCreateAccount) {
|
||||
CreateAccountSheet(session: session)
|
||||
}
|
||||
.alert("Reset Password", isPresented: $showResetPassword) {
|
||||
SecureField("New password", text: $resetPassword)
|
||||
.accessibilityLabel("New password for account")
|
||||
Button("Reset") {
|
||||
if let a = resetForAccount, !resetPassword.isEmpty {
|
||||
session.resetPassword(username: a.username, newPassword: resetPassword)
|
||||
}
|
||||
resetPassword = ""
|
||||
resetForAccount = nil
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
resetPassword = ""
|
||||
resetForAccount = nil
|
||||
}
|
||||
} message: {
|
||||
Text("Enter a new password for \(resetForAccount?.username ?? "").")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AccountRowView: View {
|
||||
let account: Account
|
||||
|
||||
private var joinedDate: String {
|
||||
let date = Date(timeIntervalSince1970: Double(account.createdAtUnixMs) / 1000)
|
||||
return date.formatted(.dateTime.year().month().day())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack {
|
||||
Text(account.username)
|
||||
.fontWeight(.medium)
|
||||
if account.isAdmin {
|
||||
Text("admin")
|
||||
.font(.caption2)
|
||||
.padding(.horizontal, 6).padding(.vertical, 2)
|
||||
.background(.orange.opacity(0.2), in: Capsule())
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
}
|
||||
Text("Created \(joinedDate)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityLabel("\(account.username)\(account.isAdmin ? ", administrator" : ""), created \(joinedDate)")
|
||||
}
|
||||
}
|
||||
|
||||
private struct CreateAccountSheet: View {
|
||||
@Bindable var session: SessionState
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var username = ""
|
||||
@State private var password = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section {
|
||||
TextField("Username", text: $username)
|
||||
.textContentType(.username)
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
.accessibilityLabel("Username")
|
||||
SecureField("Password", text: $password)
|
||||
.textContentType(.newPassword)
|
||||
.accessibilityLabel("Password")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Create Account")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") { dismiss() }
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("Create") {
|
||||
session.createAccount(username: username, password: password)
|
||||
dismiss()
|
||||
}
|
||||
.disabled(username.isEmpty || password.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user