Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 82 additions & 54 deletions LoopFollow.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions LoopFollow/Helpers/AppConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,34 @@ import Foundation
// Class that contains general constants used in different classes
class AppConstants {
static let APP_GROUP_ID = "group.com.$(unique_id).LoopFollow"

/// Extracts the app suffix from the bundle identifier
/// Bundle identifier format: com.$(unique_id).LoopFollow$(app_suffix)
/// Returns the suffix part (e.g., "2" for "com.example.LoopFollow2")
static var appSuffix: String {
guard let bundleId = Bundle.main.bundleIdentifier else {
return ""
}

// Extract suffix from bundle identifier
// Pattern: com.$(unique_id).LoopFollow$(app_suffix)
let pattern = "LoopFollow(.+)$"
if let regex = try? NSRegularExpression(pattern: pattern) {
let range = NSRange(location: 0, length: bundleId.utf16.count)
if let match = regex.firstMatch(in: bundleId, options: [], range: range) {
let suffixRange = match.range(at: 1)
if let swiftRange = Range(suffixRange, in: bundleId) {
let suffix = String(bundleId[swiftRange])
return suffix.isEmpty ? "" : "_\(suffix)"
}
}
}

return ""
}

/// Returns a unique identifier for this app instance based on the app suffix
static var appInstanceId: String {
return "LoopFollow\(appSuffix)"
}
}
6 changes: 0 additions & 6 deletions LoopFollow/Helpers/Views/QRCodeDisplayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ struct QRCodeDisplayView: View {
.scaleEffect(1.5)
)
}

Text("Scan this QR code with another LoopFollow app to import remote command settings")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 20)
}
.onAppear {
generateQRCode()
Expand Down
109 changes: 73 additions & 36 deletions LoopFollow/Helpers/Views/SimpleQRCodeScannerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,88 +3,125 @@

import AVFoundation
import SwiftUI
import UIKit

struct SimpleQRCodeScannerView: UIViewControllerRepresentable {
@Environment(\.presentationMode) var presentationMode
var completion: (Result<String, Error>) -> Void

// MARK: - Coordinator
func makeUIViewController(context _: Context) -> UINavigationController {
let scannerVC = SimpleQRCodeScannerViewController { result in
completion(result)
}

class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
var parent: SimpleQRCodeScannerView
var session: AVCaptureSession?
let navController = UINavigationController(rootViewController: scannerVC)

init(parent: SimpleQRCodeScannerView) {
self.parent = parent
// Apply dark mode if needed
if Storage.shared.forceDarkMode.value {
scannerVC.overrideUserInterfaceStyle = .dark
navController.overrideUserInterfaceStyle = .dark
}

func metadataOutput(_: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from _: AVCaptureConnection) {
if let session, session.isRunning {
if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
metadataObject.type == .qr,
let stringValue = metadataObject.stringValue
{
DispatchQueue.global(qos: .userInitiated).async {
session.stopRunning()
}
parent.completion(.success(stringValue))
}
}
}
return navController
}

func updateUIViewController(_: UINavigationController, context _: Context) {}
}

class SimpleQRCodeScannerViewController: UIViewController {
private var session: AVCaptureSession?
private var completion: (Result<String, Error>) -> Void

init(completion: @escaping (Result<String, Error>) -> Void) {
self.completion = completion
super.init(nibName: nil, bundle: nil)
}

// MARK: - UIViewControllerRepresentable Methods
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()

// Add cancel button
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .cancel,
target: self,
action: #selector(cancelTapped)
)
navigationItem.title = "Scan QR Code"

func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
setupCamera()
}

func makeUIViewController(context: Context) -> UIViewController {
let controller = UIViewController()
private func setupCamera() {
let session = AVCaptureSession()
context.coordinator.session = session // Assign session to coordinator
self.session = session

guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
session.canAddInput(videoInput)
else {
let error = NSError(domain: "QRCodeScannerError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to set up camera input."])
completion(.failure(error))
return controller
return
}

session.addInput(videoInput)

let metadataOutput = AVCaptureMetadataOutput()
if session.canAddOutput(metadataOutput) {
session.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main)
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
metadataOutput.metadataObjectTypes = [.qr]
} else {
let error = NSError(domain: "QRCodeScannerError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to set up metadata output."])
completion(.failure(error))
return controller
return
}

let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = controller.view.layer.bounds
previewLayer.frame = view.layer.bounds
previewLayer.videoGravity = .resizeAspectFill
controller.view.layer.addSublayer(previewLayer)
view.layer.addSublayer(previewLayer)

DispatchQueue.global(qos: .userInitiated).async {
session.startRunning()
}

return controller
}

func updateUIViewController(_: UIViewController, context _: Context) {}

func dismantleUIViewController(_: UIViewController, coordinator: Coordinator) {
@objc private func cancelTapped() {
DispatchQueue.global(qos: .userInitiated).async {
if let session = coordinator.session, session.isRunning {
if let session = self.session, session.isRunning {
session.stopRunning()
}
}
let error = NSError(domain: "QRCodeScannerError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Scanning cancelled by user."])
completion(.failure(error))
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let previewLayer = view.layer.sublayers?.first as? AVCaptureVideoPreviewLayer {
previewLayer.frame = view.layer.bounds
}
}
}

extension SimpleQRCodeScannerViewController: AVCaptureMetadataOutputObjectsDelegate {
func metadataOutput(_: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from _: AVCaptureConnection) {
if let session, session.isRunning {
if let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
metadataObject.type == .qr,
let stringValue = metadataObject.stringValue
{
DispatchQueue.global(qos: .userInitiated).async {
session.stopRunning()
}
completion(.success(stringValue))
}
}
}
}
13 changes: 13 additions & 0 deletions LoopFollow/Nightscout/NightscoutSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct NightscoutSettingsView: View {
urlSection
tokenSection
statusSection
importSection
}
.onDisappear {
viewModel.dismiss()
Expand Down Expand Up @@ -54,4 +55,16 @@ struct NightscoutSettingsView: View {
Text(viewModel.nightscoutStatus)
}
}

private var importSection: some View {
Section(header: Text("Import Settings")) {
NavigationLink(destination: ImportExportSettingsView()) {
HStack {
Image(systemName: "square.and.arrow.down")
.foregroundColor(.blue)
Text("Import Settings from QR Code or iCloud")
}
}
}
}
}
2 changes: 1 addition & 1 deletion LoopFollow/Remote/Settings/RemoteCommandSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ struct RemoteCommandSettings: Codable {
}

/// Checks if the settings are valid for the given remote type
func isValid() -> Bool {
func hasValidSettings() -> Bool {
switch remoteType {
case .none:
return true
Expand Down
85 changes: 13 additions & 72 deletions LoopFollow/Remote/Settings/RemoteSettingsView.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// LoopFollow
// RemoteSettingsView.swift

import AVFoundation
import HealthKit
import SwiftUI
import UIKit

struct RemoteSettingsView: View {
@ObservedObject var viewModel: RemoteSettingsViewModel
Expand All @@ -18,7 +20,6 @@ struct RemoteSettingsView: View {

enum AlertType {
case validation
case qrCodeError
case urlTokenValidation
case urlTokenUpdate
}
Expand Down Expand Up @@ -61,34 +62,21 @@ struct RemoteSettingsView: View {
.foregroundColor(.secondary)
}

// MARK: - QR Code Sharing Section
// MARK: - Import/Export Settings Section

Section {
if viewModel.remoteType == .none {
Button(action: {
viewModel.isShowingQRCodeScanner = true
}) {
HStack {
Image(systemName: "qrcode.viewfinder")
Text("Import Remote Settings from QR Code")
}
}
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
} else {
Button(action: {
viewModel.isShowingQRCodeDisplay = true
}) {
HStack {
Image(systemName: "qrcode")
Text("Export Remote Settings as QR Code")
}
NavigationLink(destination: ImportExportSettingsView()) {
HStack {
Image(systemName: "square.and.arrow.down")
.foregroundColor(.blue)
Text("Import/Export Settings")
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
}
.buttonStyle(.plain)
}

// MARK: - Meal Section (for TRC only)
Expand Down Expand Up @@ -294,12 +282,6 @@ struct RemoteSettingsView: View {
message: Text(alertMessage ?? "Invalid input."),
dismissButton: .default(Text("OK"))
)
case .qrCodeError:
return Alert(
title: Text("QR Code Error"),
message: Text(alertMessage ?? "An error occurred while processing the QR code."),
dismissButton: .default(Text("OK"))
)
case .urlTokenValidation:
return Alert(
title: Text("URL/Token Validation"),
Expand All @@ -325,30 +307,6 @@ struct RemoteSettingsView: View {
viewModel.handleLoopAPNSQRCodeScanResult(result)
}
}
.sheet(isPresented: $viewModel.isShowingQRCodeScanner) {
SimpleQRCodeScannerView { result in
viewModel.handleRemoteCommandQRCodeScanResult(result)
}
}
.sheet(isPresented: $viewModel.isShowingQRCodeDisplay) {
NavigationView {
VStack {
if let qrCodeString = viewModel.generateQRCodeForCurrentSettings() {
QRCodeDisplayView(qrCodeString: qrCodeString)
.padding()
} else {
Text("Failed to generate QR code")
.foregroundColor(.red)
.padding()
}
}
.navigationTitle("Share Remote Settings")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing: Button("Done") {
viewModel.isShowingQRCodeDisplay = false
})
}
}
.sheet(isPresented: $viewModel.showURLTokenValidation) {
NavigationView {
URLTokenValidationView(
Expand Down Expand Up @@ -380,15 +338,6 @@ struct RemoteSettingsView: View {
let now = Date().timeIntervalSince1970
otpTimeRemaining = Int(otpPeriod - (now.truncatingRemainder(dividingBy: otpPeriod)))
}
.onReceive(viewModel.$qrCodeErrorMessage) { errorMessage in
if let errorMessage = errorMessage, !errorMessage.isEmpty {
handleQRCodeError(errorMessage)
// Clear the error message after showing the alert
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
viewModel.qrCodeErrorMessage = nil
}
}
}
.onReceive(viewModel.$showURLTokenValidation) { showValidation in
if showValidation {
// The sheet will be shown automatically due to the binding
Expand Down Expand Up @@ -429,14 +378,6 @@ struct RemoteSettingsView: View {
showAlert = true
}

// MARK: - QR Code Error Handler

private func handleQRCodeError(_ message: String) {
alertMessage = message
alertType = .qrCodeError
showAlert = true
}

private var guardrailsSection: some View {
Section(header: Text("Guardrails")) {
HStack {
Expand Down
Loading