Challenge-response, SwiftData, new UI

This commit is contained in:
Christoph Hagen
2023-12-12 17:33:42 +01:00
parent 7a443d51b3
commit 941aebd9ca
51 changed files with 1741 additions and 1674 deletions

View File

@ -1,114 +1,55 @@
import Foundation
import SwiftData
struct HistoryItem {
@Model
final class HistoryItem {
/// The sent/received date (local time, not including compensation offset)
let requestDate: Date
let request: Message.Content
let startDate: Date
let usedLocalConnection: Bool
let message: Message
var response: ClientState
let route: TransmissionType
let responseMessage: Message.Content?
let finishDate: Date
let responseDate: Date
init(sent message: Message.Content, sentDate: Date, local: Bool, response: ClientState, responseDate: Date, responseMessage: Message.Content?) {
self.requestDate = sentDate
self.request = message
self.responseMessage = responseMessage
self.response = response
self.responseDate = responseDate
self.usedLocalConnection = local
init(message: Message, startDate: Date, route: TransmissionType, finishDate: Date) {
self.startDate = startDate
self.message = message
self.finishDate = finishDate
self.route = route
}
// MARK: Statistics
var roundTripTime: TimeInterval {
responseDate.timeIntervalSince(requestDate)
}
var deviceTime: Date? {
guard let timestamp = responseMessage?.time else {
return nil
}
return Date(timestamp: timestamp)
}
var requestLatency: TimeInterval? {
deviceTime?.timeIntervalSince(requestDate)
}
var responseLatency: TimeInterval? {
guard let deviceTime = deviceTime else {
return nil
}
return responseDate.timeIntervalSince(deviceTime)
}
var clockOffset: Int? {
guard let deviceTime = deviceTime else {
return nil
}
let estimatedArrival = requestDate.advanced(by: roundTripTime / 2)
return Int(deviceTime.timeIntervalSince(estimatedArrival))
}
}
extension HistoryItem: Codable {
enum CodingKeys: Int, CodingKey {
case requestDate = 1
case request = 2
case usedLocalConnection = 3
case response = 4
case responseMessage = 5
case responseDate = 6
}
}
extension ClientState: Codable {
init(from decoder: Decoder) throws {
let code = try decoder.singleValueContainer().decode(UInt8.self)
self.init(code: code)
finishDate.timeIntervalSince(startDate)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(code)
var response: MessageResult {
message.result
}
}
extension HistoryItem: Identifiable {
var id: UInt32 {
requestDate.timestamp
var id: Double {
startDate.timeIntervalSince1970
}
}
extension HistoryItem: Comparable {
static func < (lhs: HistoryItem, rhs: HistoryItem) -> Bool {
lhs.requestDate < rhs.requestDate
lhs.startDate < rhs.startDate
}
}
extension HistoryItem {
static var mock: HistoryItem {
let content = Message.Content(time: Date.now.timestamp, id: 123, device: 0)
let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124, device: 0)
let message = Message(messageType: .request, clientChallenge: 123, serverChallenge: 234, result: .unlocked)
return .init(
sent: content,
sentDate: .now,
local: false,
response: .openSesame,
responseDate: .now + 2,
responseMessage: content2)
message: message,
startDate: Date.now.addingTimeInterval(-5),
route: .throughServer,
finishDate: Date.now)
}
}

View File

@ -1,6 +1,8 @@
import SwiftUI
import SwiftData
import SFSafeSymbols
private let df: DateFormatter = {
let df = DateFormatter()
df.dateStyle = .short
@ -13,60 +15,55 @@ struct HistoryListItem: View {
let entry: HistoryItem
var entryTime: String {
df.string(from: entry.requestDate)
df.string(from: entry.startDate)
}
var roundTripText: String {
"\(Int(entry.roundTripTime * 1000)) ms"
}
var counterText: String {
let sentCounter = entry.request.id
let startText = "\(sentCounter)"
guard let rCounter = entry.responseMessage?.id else {
return startText
}
let diff = Int(rCounter) - Int(sentCounter)
guard diff != 1 && diff != 0 else {
return startText
}
return startText + " (\(diff))"
var clientNonceText: String {
"\(entry.message.clientChallenge)"
}
var timeOffsetText: String? {
guard let offset = entry.clockOffset else {
return nil
}
return "\(offset) s"
var serverNonceText: String {
"\(entry.message.serverChallenge)"
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Image(systemSymbol: entry.route.symbol)
Text(entry.response.description)
.font(.headline)
Spacer()
Text(entryTime)
}.padding(.bottom, 1)
}
HStack {
Image(systemSymbol: entry.usedLocalConnection ? .wifi : .network)
Text(roundTripText)
.font(.subheadline)
Image(systemSymbol: .personalhotspot)
Text(counterText)
.font(.subheadline)
if let timeOffsetText {
Image(systemSymbol: .stopwatch)
Text(timeOffsetText)
.font(.subheadline)
}
}.foregroundColor(.secondary)
Image(systemSymbol: .arrowUpArrowDownCircle)
Text(roundTripText).padding(.trailing)
Image(systemSymbol: .lockIphone)
Text(clientNonceText).padding(.trailing)
Image(systemSymbol: .doorRightHandClosed)
Text(serverNonceText).padding(.trailing)
}
.foregroundColor(.secondary)
.font(.footnote)
}
}
}
struct HistoryListItem_Previews: PreviewProvider {
static var previews: some View {
HistoryListItem(entry: .mock)
#Preview {
do {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: HistoryItem.self, configurations: config)
let item = HistoryItem.mock
container.mainContext.insert(item)
try container.mainContext.save()
return HistoryListItem(entry: item)
.modelContainer(container)
} catch {
fatalError("Failed to create model container.")
}
}

View File

@ -0,0 +1,61 @@
import SwiftUI
import SwiftData
struct HistoryView: View {
@Query
private var items: [HistoryItem] = []
private var unlockCount: Int {
items.count { $0.response == .unlocked }
}
private var percentage: Double {
guard items.count > 0 else {
return 0
}
return Double(unlockCount * 100) / Double(items.count)
}
private var requestNumberText: String {
guard items.count != 1 else {
return "1 Request"
}
return "\(items.count) Requests"
}
var body: some View {
NavigationView {
List {
HStack {
Text(requestNumberText)
.foregroundColor(.primary)
.font(.body)
Spacer()
Text(String(format: "%d successful (%.1f %%)", unlockCount, percentage))
.foregroundColor(.secondary)
.font(.footnote)
}
ForEach(items) {entry in
HistoryListItem(entry: entry)
}
}
.navigationTitle("History")
}
}
}
#Preview {
do {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: HistoryItem.self, configurations: config)
let item = HistoryItem.mock
container.mainContext.insert(item)
try container.mainContext.save()
return HistoryView()
.modelContainer(container)
} catch {
fatalError("Failed to create model container.")
}
}