Skip to content

Commit a8d78f2

Browse files
authored
Add sharing remote settings with QR code (#452)
1 parent 335cbf1 commit a8d78f2

File tree

9 files changed

+724
-0
lines changed

9 files changed

+724
-0
lines changed

LoopFollow.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
654134182E1DC09700BDBE08 /* OverridePresetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134172E1DC09700BDBE08 /* OverridePresetsView.swift */; };
1414
6541341A2E1DC27900BDBE08 /* OverridePresetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654134192E1DC27900BDBE08 /* OverridePresetData.swift */; };
1515
6541341C2E1DC28000BDBE08 /* DateExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6541341B2E1DC28000BDBE08 /* DateExtensions.swift */; };
16+
656F8C102E49F36F0008DC1D /* QRCodeDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */; };
17+
656F8C122E49F3780008DC1D /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */; };
18+
656F8C142E49F3D20008DC1D /* RemoteCommandSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */; };
19+
65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; };
1620
6584B1012E4A263900135D4D /* TOTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6584B1002E4A263900135D4D /* TOTPService.swift */; };
1721
65E8A2862E44B0300065037B /* VolumeButtonHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */; };
1822
DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; };
@@ -399,6 +403,10 @@
399403
654134172E1DC09700BDBE08 /* OverridePresetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetsView.swift; sourceTree = "<group>"; };
400404
654134192E1DC27900BDBE08 /* OverridePresetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridePresetData.swift; sourceTree = "<group>"; };
401405
6541341B2E1DC28000BDBE08 /* DateExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtensions.swift; sourceTree = "<group>"; };
406+
656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeDisplayView.swift; sourceTree = "<group>"; };
407+
656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = "<group>"; };
408+
656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteCommandSettings.swift; sourceTree = "<group>"; };
409+
65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = "<group>"; };
402410
6584B1002E4A263900135D4D /* TOTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TOTPService.swift; sourceTree = "<group>"; };
403411
65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtonHandler.swift; sourceTree = "<group>"; };
404412
A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -916,6 +924,8 @@
916924
DD4878062C7B2E9E0048F05C /* Settings */ = {
917925
isa = PBXGroup;
918926
children = (
927+
65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */,
928+
656F8C132E49F3D20008DC1D /* RemoteCommandSettings.swift */,
919929
DD4878072C7B30BF0048F05C /* RemoteSettingsView.swift */,
920930
DD4878092C7B30D40048F05C /* RemoteSettingsViewModel.swift */,
921931
);
@@ -1220,6 +1230,7 @@
12201230
DDF6999C2C5AAA4C0058A8D9 /* Views */ = {
12211231
isa = PBXGroup;
12221232
children = (
1233+
656F8C0F2E49F36F0008DC1D /* QRCodeDisplayView.swift */,
12231234
DDE75D2C2DE71401007C1FC1 /* TogglableSecureInput.swift */,
12241235
DDE75D222DE5E505007C1FC1 /* Glyph.swift */,
12251236
DD8316492DE4C504004467AA /* SettingsStepperRow.swift */,
@@ -1489,6 +1500,7 @@
14891500
FCC688542489367300A0279D /* Helpers */ = {
14901501
isa = PBXGroup;
14911502
children = (
1503+
656F8C112E49F3780008DC1D /* QRCodeGenerator.swift */,
14921504
DD4A407D2E6AFEE6007B318B /* AuthService.swift */,
14931505
DD1D52B82E1EB5DC00432050 /* TabPosition.swift */,
14941506
DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */,
@@ -1880,10 +1892,12 @@
18801892
DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */,
18811893
DD7F4C0F2DD51EC200D449E9 /* TempTargetStartCondition.swift in Sources */,
18821894
DDBD19962DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift in Sources */,
1895+
656F8C102E49F36F0008DC1D /* QRCodeDisplayView.swift in Sources */,
18831896
DDC6CA3D2DD7C6090060EE25 /* TemporaryCondition.swift in Sources */,
18841897
DD9ACA0E2D340BFF00415D8A /* AlarmTask.swift in Sources */,
18851898
DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */,
18861899
DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */,
1900+
656F8C122E49F3780008DC1D /* QRCodeGenerator.swift in Sources */,
18871901
6541341A2E1DC27900BDBE08 /* OverridePresetData.swift in Sources */,
18881902
FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */,
18891903
DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */,
@@ -1900,6 +1914,7 @@
19001914
DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */,
19011915
DDCC3A4B2DDBB5E4006F1C10 /* BatteryCondition.swift in Sources */,
19021916
DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */,
1917+
656F8C142E49F3D20008DC1D /* RemoteCommandSettings.swift in Sources */,
19031918
DD12D4872E1705E6004E0112 /* AlarmsContainerView.swift in Sources */,
19041919
DD83164A2DE4C504004467AA /* SettingsStepperRow.swift in Sources */,
19051920
DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */,
@@ -2053,6 +2068,7 @@
20532068
DD0650EF2DCE96FF004D3B41 /* HighBGCondition.swift in Sources */,
20542069
DDC6CA472DD8D9010060EE25 /* PumpChangeAlarmEditor.swift in Sources */,
20552070
DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */,
2071+
65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */,
20562072
DD0C0C682C48529400DBADDF /* Metric.swift in Sources */,
20572073
FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */,
20582074
DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */,
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// LoopFollow
2+
// QRCodeGenerator.swift
3+
// Created by codebymini.
4+
5+
import CoreImage
6+
import UIKit
7+
8+
enum QRCodeGenerator {
9+
/// Generates a QR code image from a string
10+
/// - Parameters:
11+
/// - string: The string to encode in the QR code
12+
/// - size: The size of the generated image (default: 200x200)
13+
/// - correctionLevel: The error correction level (default: .M)
14+
/// - Returns: A UIImage containing the QR code, or nil if generation fails
15+
static func generateQRCode(
16+
from string: String,
17+
size: CGSize = CGSize(width: 200, height: 200),
18+
correctionLevel: String = "M"
19+
) -> UIImage? {
20+
// Create a CIFilter for QR code generation
21+
guard let filter = CIFilter(name: "CIQRCodeGenerator") else {
22+
return nil
23+
}
24+
25+
// Set the input data (the string to encode)
26+
let data = string.data(using: .utf8)
27+
filter.setValue(data, forKey: "inputMessage")
28+
29+
// Set the error correction level
30+
filter.setValue(correctionLevel, forKey: "inputCorrectionLevel")
31+
32+
// Get the output image
33+
guard let outputImage = filter.outputImage else {
34+
return nil
35+
}
36+
37+
// Scale the image to the desired size
38+
let scaleX = size.width / outputImage.extent.size.width
39+
let scaleY = size.height / outputImage.extent.size.height
40+
let scaledImage = outputImage.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY))
41+
42+
// Convert CIImage to UIImage
43+
let context = CIContext()
44+
guard let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) else {
45+
return nil
46+
}
47+
48+
return UIImage(cgImage: cgImage)
49+
}
50+
51+
/// Generates a QR code image with custom colors
52+
/// - Parameters:
53+
/// - string: The string to encode in the QR code
54+
/// - size: The size of the generated image (default: 200x200)
55+
/// - foregroundColor: The color of the QR code (default: black)
56+
/// - backgroundColor: The background color (default: white)
57+
/// - correctionLevel: The error correction level (default: .M)
58+
/// - Returns: A UIImage containing the QR code, or nil if generation fails
59+
static func generateQRCode(
60+
from string: String,
61+
size: CGSize = CGSize(width: 200, height: 200),
62+
foregroundColor: UIColor = .black,
63+
backgroundColor: UIColor = .white,
64+
correctionLevel: String = "M"
65+
) -> UIImage? {
66+
// First generate the basic QR code
67+
guard let qrCodeImage = generateQRCode(from: string, size: size, correctionLevel: correctionLevel) else {
68+
return nil
69+
}
70+
71+
// Create a new image context with the desired size
72+
UIGraphicsBeginImageContextWithOptions(size, false, 0)
73+
defer { UIGraphicsEndImageContext() }
74+
75+
guard let context = UIGraphicsGetCurrentContext() else {
76+
return nil
77+
}
78+
79+
// Fill the background
80+
backgroundColor.setFill()
81+
context.fill(CGRect(origin: .zero, size: size))
82+
83+
// Draw the QR code with the foreground color
84+
context.setFillColor(foregroundColor.cgColor)
85+
context.setBlendMode(.sourceIn)
86+
87+
// Create a mask from the original QR code
88+
if let cgImage = qrCodeImage.cgImage {
89+
let maskImage = UIImage(cgImage: cgImage)
90+
maskImage.draw(in: CGRect(origin: .zero, size: size))
91+
}
92+
93+
return UIGraphicsGetImageFromCurrentImageContext()
94+
}
95+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// LoopFollow
2+
// QRCodeDisplayView.swift
3+
// Created by codebymini.
4+
5+
import SwiftUI
6+
import UIKit
7+
8+
struct QRCodeDisplayView: View {
9+
let qrCodeString: String
10+
let size: CGSize
11+
let foregroundColor: UIColor
12+
let backgroundColor: UIColor
13+
14+
@State private var qrCodeImage: UIImage?
15+
16+
init(
17+
qrCodeString: String,
18+
size: CGSize = CGSize(width: 250, height: 250),
19+
foregroundColor: UIColor = .black,
20+
backgroundColor: UIColor = .white
21+
) {
22+
self.qrCodeString = qrCodeString
23+
self.size = size
24+
self.foregroundColor = foregroundColor
25+
self.backgroundColor = backgroundColor
26+
}
27+
28+
var body: some View {
29+
VStack(spacing: 16) {
30+
if let qrCodeImage = qrCodeImage {
31+
Image(uiImage: qrCodeImage)
32+
.resizable()
33+
.aspectRatio(contentMode: .fit)
34+
.frame(width: size.width, height: size.height)
35+
.cornerRadius(12)
36+
.shadow(radius: 4)
37+
} else {
38+
RoundedRectangle(cornerRadius: 12)
39+
.fill(Color.gray.opacity(0.3))
40+
.frame(width: size.width, height: size.height)
41+
.overlay(
42+
ProgressView()
43+
.scaleEffect(1.5)
44+
)
45+
}
46+
47+
Text("Scan this QR code with another LoopFollow app to import remote command settings")
48+
.font(.caption)
49+
.foregroundColor(.secondary)
50+
.multilineTextAlignment(.center)
51+
.padding(.horizontal, 20)
52+
}
53+
.onAppear {
54+
generateQRCode()
55+
}
56+
}
57+
58+
private func generateQRCode() {
59+
DispatchQueue.global(qos: .userInitiated).async {
60+
let image = QRCodeGenerator.generateQRCode(
61+
from: qrCodeString,
62+
size: size,
63+
foregroundColor: foregroundColor,
64+
backgroundColor: backgroundColor
65+
)
66+
67+
DispatchQueue.main.async {
68+
self.qrCodeImage = image
69+
}
70+
}
71+
}
72+
}
73+
74+
#Preview {
75+
QRCodeDisplayView(qrCodeString: "https://example.com/test")
76+
.padding()
77+
}

LoopFollow/Log/LogManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class LogManager {
2828
case volumeButtonSnooze = "Volume Button Snooze"
2929
case calendar = "Calendar"
3030
case deviceStatus = "Device Status"
31+
case remote = "Remote"
3132
}
3233

3334
init() {

0 commit comments

Comments
 (0)