Use more SwiftUI elements and clean up interface

This commit is contained in:
Milan Stute
2021-03-09 23:30:04 +01:00
parent d3b72de00c
commit cbb85d97d0
7 changed files with 108 additions and 133 deletions

View File

@@ -5,6 +5,7 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
import Combine
import Foundation
import SwiftUI
import Combine
@@ -13,19 +14,33 @@ class AccessoryController: ObservableObject {
static let shared = AccessoryController()
@Published var accessories: [Accessory]
var cancellables = [AnyCancellable]()
var saveCancellable: AnyCancellable?
var accessoryObserver: AnyCancellable?
init() {
self.accessories = KeychainController.loadAccessoriesFromKeychain()
self.accessoryObserver = self.accessories.publisher
.sink { _ in
try? self.save()
}
initObserver()
}
func initObserver() {
self.accessories.forEach({
let c = $0.objectWillChange.sink(receiveValue: { self.objectWillChange.send() })
// Important: You have to keep the returned value allocated,
// otherwise the sink subscription gets cancelled
self.cancellables.append(c)
})
self.saveCancellable = self.$accessories.sink { _ in
// FIXME: accessories actually don't change
try? self.save()
}
}
init(accessories: [Accessory]) {
self.accessories = accessories
initObserver()
}
func save() throws {
@@ -49,8 +64,6 @@ class AccessoryController: ObservableObject {
accessory.lastLocation = report?.location
accessory.locationTimestamp = report?.timestamp
self.accessories[idx] = accessory
}
}
}

View File

@@ -11,13 +11,16 @@ import Foundation
import Security
import SwiftUI
class Accessory: ObservableObject, Codable, Identifiable, Equatable {
let name: String
class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
@Published var name: String
let id: Int
let privateKey: Data
let color: Color
let icon: String
@Published var color: Color
@Published var icon: String {
didSet {
print("Setting icon")
}
}
@Published var lastLocation: CLLocation?
@Published var locationTimestamp: Date?
@@ -92,6 +95,10 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
try self.hashedPublicKey().base64EncodedString()
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
private func hashedPublicKey() throws -> Data {
let publicKey = try self.getAdvertisementKey()
var sha = SHA256()

View File

@@ -10,12 +10,17 @@ import SwiftUI
struct AccessoryListEntry: View {
var accessory: Accessory
@Binding var accessoryIcon: String
@Binding var accessoryColor: Color
@Binding var accessoryName: String
@Binding var alertType: OpenHaystackMainView.AlertType?
var delete: (Accessory) -> Void
var deployAccessoryToMicrobit: (Accessory) -> Void
var zoomOn: (Accessory) -> Void
let formatter = DateFormatter()
@State var editingName: Bool = false
func timestampView() -> some View {
formatter.dateStyle = .short
formatter.timeStyle = .short
@@ -30,33 +35,22 @@ struct AccessoryListEntry: View {
}
var body: some View {
HStack {
Circle()
.strokeBorder(accessory.color, lineWidth: 2.0)
.background(
ZStack {
Circle().fill(Color("PinColor"))
Image(systemName: accessory.icon)
.padding(3)
}
)
.frame(width: 40, height: 40)
IconSelectionView(selectedImageName: $accessoryIcon, selectedColor: $accessoryColor)
Button(
action: {
self.zoomOn(self.accessory)
},
label: {
VStack(alignment: .leading) {
Text(accessory.name)
.font(.headline)
self.timestampView()
}
.contentShape(Rectangle())
VStack(alignment: .leading) {
if self.editingName {
TextField("Enter accessory name", text: $accessoryName, onCommit: { self.editingName = false })
.font(.headline)
.textFieldStyle(RoundedBorderTextFieldStyle())
} else {
Text(accessory.name)
.font(.headline)
}
)
.buttonStyle(PlainButtonStyle())
self.timestampView()
}
Spacer()
@@ -69,9 +63,9 @@ struct AccessoryListEntry: View {
}
)
}
.contentShape(Rectangle())
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
.padding(EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0))
.contextMenu {
Button("Rename", action: { self.editingName = true })
Button("Delete", action: { self.delete(accessory) })
Divider()
Button("Copy advertisment key (Base64)", action: { self.copyPublicKey(of: accessory) })

View File

@@ -71,40 +71,6 @@ class AccessoryAnnotationView: MKAnnotationView {
self.canShowCallout = true
}
// override func draw(_ dirtyRect: NSRect) {
// guard let accessoryAnnotation = self.annotation as? AccessoryAnnotation else {
// super.draw(dirtyRect)
// return
// }
//
// let path = NSBezierPath(ovalIn: dirtyRect)
// path.lineWidth = 2.0
//
// guard let cgColor = accessoryAnnotation.accessory.color.cgColor,
// let strokeColor = NSColor(cgColor: cgColor)?.withAlphaComponent(0.8) else {return}
//
// NSColor(named: NSColor.Name("PinColor"))?.setFill()
//
// path.fill()
//
// strokeColor.setStroke()
// path.stroke()
//
// let accessory = accessoryAnnotation.accessory
//
// guard let image = NSImage(systemSymbolName: accessory.icon, accessibilityDescription: accessory.name) else {return}
//
// let ratio = image.size.width / image.size.height
// let imageWidth: CGFloat = 20
// let imageHeight = imageWidth / ratio
// let imageRect = NSRect(
// x: dirtyRect.width/2 - imageWidth/2,
// y: dirtyRect.height/2 - imageHeight/2,
// width: imageWidth, height: imageHeight)
//
// image.draw(in: imageRect)
// }
struct AccessoryPinView: View {
var accessory: Accessory

View File

@@ -10,8 +10,8 @@ import SwiftUI
struct IconSelectionView: View {
@State var showImagePicker = false
@State var color: Color = .red
@Binding var selectedImageName: String
@Binding var selectedColor: Color
var body: some View {
@@ -24,11 +24,15 @@ struct IconSelectionView: View {
},
label: {
Circle()
.strokeBorder(Color.gray, lineWidth: 0.5)
.strokeBorder(self.selectedColor, lineWidth: 2)
.background(
Image(systemName: self.selectedImageName)
ZStack {
Circle().fill(Color("PinColor"))
Image(systemName: self.selectedImageName)
}
)
.frame(width: 30, height: 30)
.frame(width: 40, height: 40)
}
)
.buttonStyle(PlainButtonStyle())
@@ -45,11 +49,12 @@ struct IconSelectionView: View {
struct ColorSelectionView_Previews: PreviewProvider {
@State static var selectedImageName: String = "briefcase.fill"
@State static var selectedColor: Color = .red
static var previews: some View {
Group {
IconSelectionView(selectedImageName: self.$selectedImageName)
ImageSelectionList(selectedImageName: self.$selectedImageName, dismiss: {})
IconSelectionView(selectedImageName: self.$selectedImageName, selectedColor: self.$selectedColor)
ImageSelectionList(selectedImageName: self.$selectedImageName, dismiss: { () })
}
}
@@ -63,24 +68,26 @@ struct ImageSelectionList: View {
let dismiss: () -> Void
var body: some View {
List(self.selectableIcons, id: \.self) { iconName in
Button(
action: {
self.selectedImageName = iconName
self.dismiss()
},
label: {
HStack {
Spacer()
Image(systemName: iconName)
Spacer()
VStack {
List(self.selectableIcons, id: \.self) { iconName in
Button(
action: {
self.selectedImageName = iconName
self.dismiss()
},
label: {
HStack {
Spacer()
Image(systemName: iconName)
Spacer()
}
}
}
)
.buttonStyle(PlainButtonStyle())
.contentShape(Rectangle())
)
.buttonStyle(PlainButtonStyle())
.contentShape(Rectangle())
}
.frame(width: 100)
}
.frame(width: 100)
}
}

View File

@@ -28,47 +28,18 @@ struct ManageAccessoriesView: View {
var body: some View {
VStack {
Text("Create a new tracking accessory")
.font(.title2)
.padding(.top)
Text("A BBC Microbit can be used to track anything you care about. Connect it over USB, name the accessory (e.g. Backpack) generate the key and deploy it")
.multilineTextAlignment(.center)
.font(.caption)
.foregroundColor(.gray)
HStack {
TextField("Name", text: self.$keyName)
ColorPicker("", selection: self.$accessoryColor)
.frame(maxWidth: 50, maxHeight: 20)
IconSelectionView(selectedImageName: self.$selectedIcon)
}
Button(
action: self.addAccessory,
label: {
Text("Generate key and deploy")
}
)
.disabled(self.keyName.isEmpty)
.padding(.bottom)
Divider()
Text("Your accessories")
.font(.title2)
.padding(.top)
if self.accessories.isEmpty {
Spacer()
Text("No accessories have been added yet. Go ahead and add one above")
Text("No accessories have been added yet. Go ahead and add one via the '+' icon.")
.multilineTextAlignment(.center)
Spacer()
} else {
self.accessoryList
}
Spacer()
}
.sheet(isPresented: self.$showESP32DeploySheet, content: {
ESP32InstallSheet(accessory: self.$accessoryToDeploy, alertType: self.$alertType)
@@ -77,16 +48,27 @@ struct ManageAccessoriesView: View {
/// Accessory List view.
var accessoryList: some View {
List(self.accessories) { accessory in
List(self.accessories, id: \.self, selection: $focusedAccessory) { accessory in
AccessoryListEntry(
accessory: accessory,
accessoryIcon: Binding(
get: { accessory.icon },
set: { accessory.icon = $0 }
),
accessoryColor: Binding(
get: { accessory.color },
set: { accessory.color = $0 }
),
accessoryName: Binding(
get: { accessory.name },
set: { accessory.name = $0 }
),
alertType: self.$alertType,
delete: self.delete(accessory:),
deployAccessoryToMicrobit: self.deploy(accessory:),
deployAccessoryToMicrobit: self.deployAccessoryToMicrobit(accessory:),
zoomOn: { self.focusedAccessory = $0 })
}
.background(Color.clear)
.cornerRadius(15.0)
.listStyle(SidebarListStyle())
}
/// Delete an accessory from the list of accessories.

View File

@@ -25,7 +25,7 @@ struct OpenHaystackMainView: View {
@State var searchPartyTokenLoaded = false
@State var mapType: MKMapType = .standard
@State var isLoading = false
@State var focusedAccessory: Accessory?
@State var focusedAccessory: Accessory? = nil
@State var accessoryToDeploy: Accessory?
@State var showESP32DeploySheet = false
@@ -33,25 +33,32 @@ struct OpenHaystackMainView: View {
var body: some View {
NavigationView {
ManageAccessoriesView(
alertType: self.$alertType,
focusedAccessory: self.$focusedAccessory,
accessoryToDeploy: self.$accessoryToDeploy,
showESP32DeploySheet: self.$showESP32DeploySheet)
.toolbar(content: {
Spacer()
Button(action: self.addAccessory) {
Label("Add accessory", systemImage: "plus")
}
})
.navigationTitle(self.focusedAccessory?.name ?? "OpenHaystack")
VStack {
ZStack {
self.mapView
if self.popUpAlertType != nil {
VStack {
Spacer()
PopUpAlertView(alertType: self.popUpAlertType!)
.transition(AnyTransition.move(edge: .bottom))
.padding(.bottom, 30)
}
}
}
.ignoresSafeArea(.all)
.alert(
item: self.$alertType,
content: { alertType in
@@ -75,7 +82,6 @@ struct OpenHaystackMainView: View {
self.onAppear()
}
}
.navigationTitle("OpenHaystack")
}
// MARK: Subviews