|
|
|
@ -0,0 +1,339 @@
|
|
|
|
|
# WiFi功能增强实现文档
|
|
|
|
|
|
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
基于Medium文章 [Connect to WiFi from iOS App](https://medium.com/@rachnaios/connect-to-wifi-from-ios-app-37253a70aa4e) 的实现方法,我们为MyQrCode应用添加了完整的WiFi自动连接功能。用户扫描WiFi二维码后,可以一键复制密码并自动连接到WiFi网络。
|
|
|
|
|
|
|
|
|
|
## 功能特性
|
|
|
|
|
|
|
|
|
|
### 1. 扫描WiFi二维码
|
|
|
|
|
用户扫描包含WiFi信息的二维码后,可以:
|
|
|
|
|
- 查看WiFi网络信息(SSID、加密方式、密码状态)
|
|
|
|
|
- 一键复制WiFi密码
|
|
|
|
|
- 智能WiFi设置:
|
|
|
|
|
- 使用NEHotspotConfiguration进行WiFi自动连接(iOS 11+)
|
|
|
|
|
- 降级到系统WiFi设置页面
|
|
|
|
|
- 最终显示详细的手动设置指导
|
|
|
|
|
|
|
|
|
|
### 2. 历史记录管理
|
|
|
|
|
在历史记录中查看WiFi二维码时:
|
|
|
|
|
- 重新获取WiFi密码
|
|
|
|
|
- 智能WiFi设置(支持NEHotspotConfiguration和降级方案)
|
|
|
|
|
- 管理收藏的WiFi信息
|
|
|
|
|
|
|
|
|
|
### 3. 便捷操作
|
|
|
|
|
- **一键复制密码**: 快速复制WiFi密码到剪贴板
|
|
|
|
|
- **智能WiFi设置**:
|
|
|
|
|
- iOS 11+:使用NEHotspotConfiguration进行WiFi自动连接
|
|
|
|
|
- 降级方案:跳转到系统WiFi设置页面
|
|
|
|
|
- 最终降级:显示详细的手动设置指导,包含网络名称和密码
|
|
|
|
|
- **用户反馈**: 操作后显示确认提示
|
|
|
|
|
|
|
|
|
|
### 4. 用户体验
|
|
|
|
|
- **视觉区分**: 使用不同颜色的图标区分功能
|
|
|
|
|
- **直观操作**: 图标按钮功能一目了然
|
|
|
|
|
- **智能降级**: 根据系统版本和权限提供最佳的用户体验
|
|
|
|
|
- **标准API**: 使用iOS官方NEHotspotConfiguration API,确保可靠性和兼容性
|
|
|
|
|
- **详细指导**: 提供完整的手动设置步骤和网络信息
|
|
|
|
|
|
|
|
|
|
## 技术实现
|
|
|
|
|
|
|
|
|
|
### 1. 数据结构扩展
|
|
|
|
|
|
|
|
|
|
#### WiFiDetails结构体
|
|
|
|
|
```swift
|
|
|
|
|
struct WiFiDetails: Codable {
|
|
|
|
|
let ssid: String
|
|
|
|
|
let password: String
|
|
|
|
|
let encryption: String
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### ParsedQRData扩展
|
|
|
|
|
```swift
|
|
|
|
|
public struct ParsedQRData: NSSecureCoding {
|
|
|
|
|
// ... 现有属性
|
|
|
|
|
public let extraData: Data? // 新增:存储WiFi详细信息
|
|
|
|
|
|
|
|
|
|
// 更新初始化方法以支持extraData
|
|
|
|
|
public init(type: QRCodeType, content: String, extraData: Data? = nil) {
|
|
|
|
|
self.type = type
|
|
|
|
|
self.content = content
|
|
|
|
|
self.extraData = extraData
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 2. WiFi连接管理器
|
|
|
|
|
|
|
|
|
|
创建了专门的`WiFiConnectionManager`类来管理WiFi连接:
|
|
|
|
|
|
|
|
|
|
```swift
|
|
|
|
|
class WiFiConnectionManager: ObservableObject {
|
|
|
|
|
@Published var isConnecting = false
|
|
|
|
|
@Published var connectionStatus: ConnectionStatus = .idle
|
|
|
|
|
|
|
|
|
|
enum ConnectionStatus {
|
|
|
|
|
case idle
|
|
|
|
|
case connecting
|
|
|
|
|
case connected
|
|
|
|
|
case failed(String)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static let shared = WiFiConnectionManager()
|
|
|
|
|
|
|
|
|
|
func connectToWiFi(ssid: String, password: String, completion: @escaping (Bool, String?) -> Void) {
|
|
|
|
|
guard #available(iOS 11.0, *) else {
|
|
|
|
|
completion(false, "iOS 11+ required for WiFi connection")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.isConnecting = true
|
|
|
|
|
self.connectionStatus = .connecting
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 创建WiFi配置
|
|
|
|
|
let configuration = NEHotspotConfiguration(ssid: ssid, passphrase: password, isWEP: false)
|
|
|
|
|
configuration.joinOnce = true
|
|
|
|
|
|
|
|
|
|
// 应用配置
|
|
|
|
|
NEHotspotConfigurationManager.shared.apply(configuration) { [weak self] error in
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self?.isConnecting = false
|
|
|
|
|
|
|
|
|
|
if let error = error {
|
|
|
|
|
let errorMessage = self?.handleWiFiError(error)
|
|
|
|
|
self?.connectionStatus = .failed(errorMessage ?? "Unknown error")
|
|
|
|
|
completion(false, errorMessage)
|
|
|
|
|
} else {
|
|
|
|
|
self?.connectionStatus = .connected
|
|
|
|
|
completion(true, nil)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func handleWiFiError(_ error: Error) -> String {
|
|
|
|
|
let errorCode = (error as NSError).code
|
|
|
|
|
|
|
|
|
|
switch errorCode {
|
|
|
|
|
case NEHotspotConfigurationError.userDenied.rawValue:
|
|
|
|
|
return "wifi_user_denied".localized
|
|
|
|
|
case NEHotspotConfigurationError.alreadyAssociated.rawValue:
|
|
|
|
|
return "wifi_already_connected".localized
|
|
|
|
|
case NEHotspotConfigurationError.invalidSSID.rawValue:
|
|
|
|
|
return "wifi_invalid_ssid".localized
|
|
|
|
|
case NEHotspotConfigurationError.invalidWPAPassphrase.rawValue:
|
|
|
|
|
return "wifi_invalid_password".localized
|
|
|
|
|
default:
|
|
|
|
|
return "wifi_connection_failed".localized
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 智能降级策略
|
|
|
|
|
```swift
|
|
|
|
|
func connectWithFallback(ssid: String, password: String, completion: @escaping (Bool, String?) -> Void) {
|
|
|
|
|
connectToWiFi(ssid: ssid, password: password) { [weak self] success, error in
|
|
|
|
|
if success {
|
|
|
|
|
completion(true, nil)
|
|
|
|
|
} else {
|
|
|
|
|
// 如果NEHotspotConfiguration失败,尝试降级方案
|
|
|
|
|
self?.tryFallbackConnection(ssid: ssid, password: password, completion: completion)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func tryFallbackConnection(ssid: String, password: String, completion: @escaping (Bool, String?) -> Void) {
|
|
|
|
|
// 尝试打开系统WiFi设置
|
|
|
|
|
let systemWifiURLString = "App-Prefs:root=WIFI"
|
|
|
|
|
if let url = URL(string: systemWifiURLString) {
|
|
|
|
|
if UIApplication.shared.canOpenURL(url) {
|
|
|
|
|
UIApplication.shared.open(url) { success in
|
|
|
|
|
if success {
|
|
|
|
|
completion(false, "wifi_opened_settings".localized)
|
|
|
|
|
} else {
|
|
|
|
|
completion(false, String(format: "wifi_manual_setup_instruction".localized, ssid, password))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 最终降级方案:显示手动设置指导
|
|
|
|
|
completion(false, String(format: "wifi_manual_setup_instruction".localized, ssid, password))
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 3. 解析器更新
|
|
|
|
|
|
|
|
|
|
#### QRCodeParser扩展
|
|
|
|
|
```swift
|
|
|
|
|
private func parseWiFi(_ content: String) -> ParsedQRData? {
|
|
|
|
|
// WiFi格式: WIFI:S:<SSID>;T:<WPA|WEP|>;P:<password>;;
|
|
|
|
|
let pattern = "WIFI:S:([^;]+);T:([^;]*);P:([^;]+);;"
|
|
|
|
|
|
|
|
|
|
guard let regex = try? NSRegularExpression(pattern: pattern),
|
|
|
|
|
let match = regex.firstMatch(in: content, range: NSRange(content.startIndex..., in: content)) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ssid = String(content[Range(match.range(at: 1), in: content)!])
|
|
|
|
|
let encryption = String(content[Range(match.range(at: 2), in: content)!])
|
|
|
|
|
let password = String(content[Range(match.range(at: 3), in: content)!])
|
|
|
|
|
|
|
|
|
|
// 创建WiFi详细信息
|
|
|
|
|
let wifiDetails = WiFiDetails(ssid: ssid, password: password, encryption: encryption)
|
|
|
|
|
|
|
|
|
|
// 编码为Data存储
|
|
|
|
|
let extraData = try? JSONEncoder().encode(wifiDetails)
|
|
|
|
|
|
|
|
|
|
return ParsedQRData(type: .wifi, content: content, extraData: extraData)
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 4. UI界面更新
|
|
|
|
|
|
|
|
|
|
#### QRCodeDetailView增强
|
|
|
|
|
```swift
|
|
|
|
|
// MARK: - 设置WiFi
|
|
|
|
|
private func setupWiFi() {
|
|
|
|
|
guard let wifiDetails = getWiFiDetails() else { return }
|
|
|
|
|
|
|
|
|
|
// 使用WiFi连接管理器
|
|
|
|
|
WiFiConnectionManager.shared.connectWithFallback(ssid: wifiDetails.ssid, password: wifiDetails.password) { [weak self] success, error in
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
if success {
|
|
|
|
|
self?.alertMessage = "wifi_connected_successfully".localized
|
|
|
|
|
} else {
|
|
|
|
|
self?.alertMessage = error ?? "wifi_connection_failed".localized
|
|
|
|
|
}
|
|
|
|
|
self?.showingAlert = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - 复制WiFi密码
|
|
|
|
|
private func copyWiFiPassword() {
|
|
|
|
|
guard let wifiDetails = getWiFiDetails() else { return }
|
|
|
|
|
|
|
|
|
|
UIPasteboard.general.string = wifiDetails.password
|
|
|
|
|
alertMessage = "wifi_password_copied".localized
|
|
|
|
|
showingAlert = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - 获取WiFi详情
|
|
|
|
|
private func getWiFiDetails() -> WiFiDetails? {
|
|
|
|
|
guard let extraData = historyItem.parsedData?.extraData else { return nil }
|
|
|
|
|
return try? JSONDecoder().decode(WiFiDetails.self, from: extraData)
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 条件显示WiFi按钮
|
|
|
|
|
```swift
|
|
|
|
|
// 在actionButtonsSection中条件显示WiFi按钮
|
|
|
|
|
if getQRCodeType() == .wifi {
|
|
|
|
|
// 复制WiFi密码按钮
|
|
|
|
|
Button(action: copyWiFiPassword) {
|
|
|
|
|
Image(systemName: "doc.on.doc.fill")
|
|
|
|
|
.font(.title2)
|
|
|
|
|
.foregroundColor(.orange)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 设置WiFi按钮
|
|
|
|
|
Button(action: setupWiFi) {
|
|
|
|
|
Image(systemName: "wifi.circle")
|
|
|
|
|
.font(.title2)
|
|
|
|
|
.foregroundColor(.purple)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 5. 本地化支持
|
|
|
|
|
|
|
|
|
|
#### 新增本地化字符串
|
|
|
|
|
```strings
|
|
|
|
|
// 中文
|
|
|
|
|
"wifi_password_copied" = "WiFi密码已复制到剪贴板";
|
|
|
|
|
"wifi_connected_successfully" = "WiFi连接成功!";
|
|
|
|
|
"wifi_connection_failed" = "WiFi连接失败";
|
|
|
|
|
"wifi_opened_settings" = "已打开系统WiFi设置";
|
|
|
|
|
"wifi_user_denied" = "用户拒绝了WiFi连接请求";
|
|
|
|
|
"wifi_already_connected" = "已经连接到该WiFi网络";
|
|
|
|
|
"wifi_invalid_ssid" = "无效的WiFi网络名称";
|
|
|
|
|
"wifi_invalid_password" = "WiFi密码格式无效";
|
|
|
|
|
|
|
|
|
|
// 英文
|
|
|
|
|
"wifi_password_copied" = "WiFi password copied to clipboard";
|
|
|
|
|
"wifi_connected_successfully" = "WiFi connected successfully!";
|
|
|
|
|
"wifi_connection_failed" = "WiFi connection failed";
|
|
|
|
|
"wifi_opened_settings" = "Opened system WiFi settings";
|
|
|
|
|
"wifi_user_denied" = "User denied WiFi connection request";
|
|
|
|
|
"wifi_already_connected" = "Already connected to this WiFi network";
|
|
|
|
|
"wifi_invalid_ssid" = "Invalid WiFi network name";
|
|
|
|
|
"wifi_invalid_password" = "Invalid WiFi password format";
|
|
|
|
|
|
|
|
|
|
// 泰语
|
|
|
|
|
"wifi_password_copied" = "รหัสผ่าน WiFi ถูกคัดลอกไปยังคลิปบอร์ดแล้ว";
|
|
|
|
|
"wifi_connected_successfully" = "เชื่อมต่อ WiFi สำเร็จแล้ว!";
|
|
|
|
|
"wifi_connection_failed" = "การเชื่อมต่อ WiFi ล้มเหลว";
|
|
|
|
|
"wifi_opened_settings" = "เปิดการตั้งค่า WiFi ของระบบแล้ว";
|
|
|
|
|
"wifi_user_denied" = "ผู้ใช้ปฏิเสธคำขอเชื่อมต่อ WiFi";
|
|
|
|
|
"wifi_already_connected" = "เชื่อมต่อกับเครือข่าย WiFi นี้แล้ว";
|
|
|
|
|
"wifi_invalid_ssid" = "ชื่อเครือข่าย WiFi ไม่ถูกต้อง";
|
|
|
|
|
"wifi_invalid_password" = "รูปแบบรหัสผ่าน WiFi ไม่ถูกต้อง";
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 实现亮点
|
|
|
|
|
|
|
|
|
|
### 1. 基于Medium文章的最佳实践
|
|
|
|
|
- 使用NEHotspotConfiguration API进行WiFi自动连接
|
|
|
|
|
- 实现完整的错误处理和用户反馈
|
|
|
|
|
- 采用智能降级策略确保兼容性
|
|
|
|
|
|
|
|
|
|
### 2. 架构设计
|
|
|
|
|
- 创建专门的WiFi连接管理器类
|
|
|
|
|
- 使用ObservableObject进行状态管理
|
|
|
|
|
- 实现单例模式便于全局访问
|
|
|
|
|
|
|
|
|
|
### 3. 用户体验优化
|
|
|
|
|
- 实时连接状态反馈
|
|
|
|
|
- 多语言错误提示
|
|
|
|
|
- 智能降级策略
|
|
|
|
|
- 直观的图标设计
|
|
|
|
|
|
|
|
|
|
### 4. 技术特性
|
|
|
|
|
- iOS 11+版本兼容性检查
|
|
|
|
|
- 完整的错误类型处理
|
|
|
|
|
- 异步操作和主线程UI更新
|
|
|
|
|
- 内存管理(weak self)
|
|
|
|
|
|
|
|
|
|
## 使用流程
|
|
|
|
|
|
|
|
|
|
1. **扫描WiFi二维码**: 用户扫描包含WiFi信息的二维码
|
|
|
|
|
2. **查看详情**: 在详情页面查看WiFi网络信息
|
|
|
|
|
3. **复制密码**: 点击橙色复制图标复制WiFi密码
|
|
|
|
|
4. **自动连接**: 点击紫色WiFi图标尝试自动连接
|
|
|
|
|
5. **降级处理**: 如果自动连接失败,系统会尝试打开WiFi设置或显示手动设置指导
|
|
|
|
|
|
|
|
|
|
## 错误处理
|
|
|
|
|
|
|
|
|
|
NEHotspotConfiguration支持的错误类型:
|
|
|
|
|
- **用户拒绝**: 用户拒绝了WiFi连接请求
|
|
|
|
|
- **已连接**: 已经连接到该WiFi网络
|
|
|
|
|
- **无效SSID**: 无效的WiFi网络名称
|
|
|
|
|
- **无效密码**: WiFi密码格式无效
|
|
|
|
|
|
|
|
|
|
## 总结
|
|
|
|
|
|
|
|
|
|
通过参考Medium文章的实现方法,我们成功为MyQrCode应用添加了完整的WiFi自动连接功能。该实现具有以下优势:
|
|
|
|
|
|
|
|
|
|
1. **标准API**: 使用iOS官方NEHotspotConfiguration API
|
|
|
|
|
2. **智能降级**: 多层降级策略确保所有用户都能使用
|
|
|
|
|
3. **完整错误处理**: 详细的错误类型处理和用户反馈
|
|
|
|
|
4. **多语言支持**: 支持中文、英文、泰语三种语言
|
|
|
|
|
5. **用户体验**: 直观的操作界面和实时状态反馈
|
|
|
|
|
|
|
|
|
|
这个实现为用户提供了便捷的WiFi连接体验,同时确保了在各种情况下的可靠性和兼容性。
|