Skip to content

Commit c515773

Browse files
Fix: Allow access to local network (#17)
* better * this is close * save * bump version
1 parent c5e7500 commit c515773

4 files changed

Lines changed: 320 additions & 22 deletions

File tree

RemoteCam/DeviceScannerViewController.swift

Lines changed: 284 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@
99
import UIKit
1010
import Theater
1111
import MultipeerConnectivity
12+
import Network
13+
import dnssd
1214

1315
let goToRolePickerController = "goToRolePickerController"
1416
let service: String = "RemoteCam"
1517
let userDefaultsPeerId = "peerID"
18+
let userDefaultsSpeedRunScanning = "speedrunscanning"
1619
let remoteShutterUrl = "https://apps.apple.com/us/app/remote-shutter/id633274861"
1720

1821
func generateQRCode(_ string: String) -> UIImage? {
@@ -47,10 +50,32 @@ public class DeviceScannerViewController: UIViewController {
4750

4851
var connectedPeers: [MCPeerID] = []
4952

53+
// Add state tracking
54+
var isScanning: Bool = false
55+
var hasLocalNetworkAccess: Bool = true
56+
var hasScanningError: Bool = false
57+
58+
// Modern Swift UserDefaults property
59+
var speedRunScanning: Bool {
60+
get {
61+
UserDefaults.standard.bool(forKey: userDefaultsSpeedRunScanning)
62+
}
63+
set {
64+
UserDefaults.standard.set(newValue, forKey: userDefaultsSpeedRunScanning)
65+
// Update UI when flag changes
66+
DispatchQueue.main.async {
67+
self.tableView.reloadData()
68+
}
69+
}
70+
}
71+
5072
lazy var splash = {
5173
CoolActivityIndicator(currentController: self)
5274
}()
5375

76+
// Add network browser for checking local network access
77+
var networkBrowser: NWBrowser?
78+
5479
lazy var scanner: MCNearbyServiceBrowser = {
5580
if let data = UserDefaults.standard.data(forKey: userDefaultsPeerId),
5681
let id = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? MCPeerID {
@@ -88,7 +113,18 @@ public class DeviceScannerViewController: UIViewController {
88113
public override func viewDidAppear(_ animated: Bool) {
89114
super.viewDidAppear(animated)
90115
self.remoteCamSession ! Disconnect()
91-
self.remoteCamSession ! UICmd.StartScanning(sender: nil)
116+
117+
// Reload table data in case user returned from system permission dialog
118+
DispatchQueue.main.async {
119+
self.tableView.reloadData()
120+
}
121+
122+
// Check if user has successfully found peers before (speed run mode)
123+
if speedRunScanning {
124+
// Auto-start scanning for experienced users
125+
checkLocalNetworkAccessAndStartScanning()
126+
}
127+
// First-time users will see the button and educational flow
92128
}
93129

94130
public override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
@@ -106,18 +142,167 @@ public class DeviceScannerViewController: UIViewController {
106142
func startScanning() {
107143
splash.stopAnimating()
108144
connectedPeers.removeAll()
109-
tableView.reloadData()
145+
isScanning = true
146+
hasScanningError = false // Clear error state when starting new scan
147+
DispatchQueue.main.async {
148+
self.tableView.reloadData()
149+
}
110150
scanner.stopBrowsingForPeers()
111151
scanner.startBrowsingForPeers()
112152
}
113153

114154
func stopScanning() {
115155
splash.stopAnimating()
116156
connectedPeers.removeAll()
117-
tableView.reloadData()
157+
isScanning = false
158+
DispatchQueue.main.async {
159+
self.tableView.reloadData()
160+
}
118161
scanner.stopBrowsingForPeers()
119162
}
120163

164+
@IBAction func startScanningDevices() {
165+
showScanningPermissionAlert()
166+
}
167+
168+
func showScanningPermissionAlert() {
169+
let alert = UIAlertController(
170+
title: NSLocalizedString("Scan for Nearby Devices", comment: ""),
171+
message: NSLocalizedString("Remote Shutter needs to scan for other devices on your local network to establish a connection. This allows you to use one device as a camera and another as a remote control.", comment: ""),
172+
preferredStyle: .alert
173+
)
174+
175+
alert.addAction(UIAlertAction(
176+
title: NSLocalizedString("Not Now", comment: ""),
177+
style: .cancel,
178+
handler: { _ in
179+
// Reload table when "Not Now" is tapped
180+
DispatchQueue.main.async {
181+
self.tableView.reloadData()
182+
}
183+
}
184+
))
185+
186+
alert.addAction(UIAlertAction(
187+
title: NSLocalizedString("Start Scanning", comment: ""),
188+
style: .default,
189+
handler: { _ in
190+
self.checkLocalNetworkAccessAndStartScanning()
191+
// Reload table when "Start Scanning" is tapped
192+
DispatchQueue.main.async {
193+
self.tableView.reloadData()
194+
}
195+
}
196+
))
197+
198+
present(alert, animated: true) {
199+
// Reload table after scanning permission alert is presented
200+
DispatchQueue.main.async {
201+
self.tableView.reloadData()
202+
}
203+
}
204+
}
205+
206+
func checkLocalNetworkAccessAndStartScanning() {
207+
// Use Bonjour to check local network access
208+
let parameters = NWParameters()
209+
parameters.includePeerToPeer = true
210+
211+
let browserDescriptor = NWBrowser.Descriptor.bonjourWithTXTRecord(
212+
type: "_remotecam._tcp",
213+
domain: "local."
214+
)
215+
216+
networkBrowser = NWBrowser(for: browserDescriptor, using: parameters)
217+
218+
networkBrowser?.stateUpdateHandler = { [weak self] state in
219+
DispatchQueue.main.async {
220+
switch state {
221+
case .waiting(let error):
222+
print("Network browser waiting with error: \(error)")
223+
if case .dns(let dnsError) = error {
224+
let dnsCode = Int(dnsError)
225+
print("DNS error code: \(dnsCode)")
226+
if dnsCode == Int(kDNSServiceErr_PolicyDenied) {
227+
// No local network access - reset speed run mode and set error state
228+
self?.speedRunScanning = false
229+
self?.hasLocalNetworkAccess = false
230+
self?.hasScanningError = true
231+
self?.showLocalNetworkAccessDeniedAlert()
232+
return
233+
}
234+
}
235+
// Other waiting states - continue with scanning, clear error state
236+
self?.hasLocalNetworkAccess = true
237+
self?.hasScanningError = false
238+
self?.startActualScanning()
239+
case .ready:
240+
print("Network browser ready")
241+
// Network access is available - clear error state
242+
self?.hasLocalNetworkAccess = true
243+
self?.hasScanningError = false
244+
self?.startActualScanning()
245+
case .failed(let error):
246+
print("Network browser failed: \(error)")
247+
// Permission denied - likely local network access denied
248+
self?.speedRunScanning = false
249+
self?.hasLocalNetworkAccess = false
250+
self?.hasScanningError = true
251+
self?.showLocalNetworkAccessDeniedAlert()
252+
default:
253+
print("Network browser state: \(state)")
254+
break
255+
}
256+
}
257+
}
258+
259+
networkBrowser?.start(queue: .main)
260+
261+
// Stop the browser after a short check
262+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
263+
self.networkBrowser?.cancel()
264+
}
265+
}
266+
267+
func startActualScanning() {
268+
self.remoteCamSession ! UICmd.StartScanning(sender: nil)
269+
startScanning()
270+
}
271+
272+
func showLocalNetworkAccessDeniedAlert() {
273+
let alert = UIAlertController(
274+
title: NSLocalizedString("Local Network Access Required", comment: ""),
275+
message: NSLocalizedString("Remote Shutter needs access to your local network to find other devices. Please grant access in Settings.", comment: ""),
276+
preferredStyle: .alert
277+
)
278+
279+
alert.addAction(UIAlertAction(
280+
title: NSLocalizedString("Cancel", comment: ""),
281+
style: .cancel,
282+
handler: { _ in
283+
// Reload table when "Cancel" is tapped
284+
DispatchQueue.main.async {
285+
self.tableView.reloadData()
286+
}
287+
}
288+
))
289+
290+
alert.addAction(UIAlertAction(
291+
title: NSLocalizedString("Open Settings", comment: ""),
292+
style: .default,
293+
handler: { _ in
294+
self.goToAppSettings()
295+
}
296+
))
297+
298+
present(alert, animated: true) {
299+
// Reload table after local network access alert is presented
300+
DispatchQueue.main.async {
301+
self.tableView.reloadData()
302+
}
303+
}
304+
}
305+
121306
@IBAction func goToRolePicker() {
122307
self.performSegue(withIdentifier: goToRolePickerController, sender: self)
123308
}
@@ -141,6 +326,7 @@ public class DeviceScannerViewController: UIViewController {
141326

142327
deinit {
143328
print("deinit DeviceScanners")
329+
networkBrowser?.cancel()
144330
frameSender ! Actor.Harakiri(sender: nil)
145331
remoteCamSession ! Actor.Harakiri(sender: nil)
146332
}
@@ -159,27 +345,59 @@ extension DeviceScannerViewController: UITableViewDataSource, UITableViewDelegat
159345
} else {
160346
let cell = tableView.dequeueReusableCell(withIdentifier: "instructions") as! DeviceScannerPlaceholder
161347
cell.qrCode.image = qrCodeImage
162-
cell.shareButton.styleButton(
163-
backgroundColor: UIColor.systemGray,
164-
borderColor: UIColor.clear,
165-
textColor: UIColor.white
166-
)
167-
cell.shareButton.setNeedsDisplay()
348+
349+
// If no local network access, hide scanning buttons and only show settings
350+
if !hasLocalNetworkAccess {
351+
cell.shareButton.isHidden = true
352+
cell.goToSettings.isHidden = false
353+
} else {
354+
// Configure start scanning button when network access is available
355+
cell.shareButton.isHidden = false
356+
if !isScanning {
357+
cell.shareButton.setTitle(NSLocalizedString("Start Scanning Devices", comment: ""), for: .normal)
358+
cell.shareButton.removeTarget(nil, action: nil, for: .allEvents)
359+
cell.shareButton.addTarget(self, action: #selector(startScanningDevices), for: .touchUpInside)
360+
cell.shareButton.styleButton(
361+
backgroundColor: UIColor.systemGreen,
362+
borderColor: UIColor.clear,
363+
textColor: UIColor.white
364+
)
365+
} else {
366+
cell.shareButton.setTitle(NSLocalizedString("Stop Scanning", comment: ""), for: .normal)
367+
cell.shareButton.removeTarget(nil, action: nil, for: .allEvents)
368+
cell.shareButton.addTarget(self, action: #selector(stopScanningDevices), for: .touchUpInside)
369+
cell.shareButton.styleButton(
370+
backgroundColor: UIColor.systemRed,
371+
borderColor: UIColor.clear,
372+
textColor: UIColor.white
373+
)
374+
}
375+
cell.shareButton.setNeedsDisplay()
376+
377+
// Configure settings button - only show when there's a scanning error
378+
if #available(iOS 14.0, *) {
379+
cell.goToSettings.isHidden = !hasScanningError
380+
} else {
381+
cell.goToSettings.isHidden = true
382+
}
383+
}
384+
385+
// Always configure settings button styling when visible
168386
cell.goToSettings.styleButton(
169-
backgroundColor: UIColor.systemGreen,
387+
backgroundColor: UIColor.systemGray,
170388
borderColor: UIColor.clear,
171389
textColor: UIColor.white
172390
)
173391
cell.goToSettings.setNeedsDisplay()
174-
if #available(iOS 14.0, *) {
175-
cell.goToSettings.isHidden = false
176-
} else {
177-
cell.goToSettings.isHidden = true
178-
}
179392
return cell
180393
}
181394
}
182395

396+
@objc func stopScanningDevices() {
397+
stopScanning()
398+
self.remoteCamSession ! Disconnect()
399+
}
400+
183401
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
184402
tableView.deselectRow(at: indexPath, animated: true)
185403
if (connectedPeers.count == 0) {
@@ -190,19 +408,67 @@ extension DeviceScannerViewController: UITableViewDataSource, UITableViewDelegat
190408
}
191409

192410
public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
193-
return "SEARCHING FOR NEARBY DEVICES..."
411+
if !isScanning {
412+
if hasScanningError {
413+
return NSLocalizedString("SCANNING ERROR - CHECK NETWORK SETTINGS", comment: "")
414+
} else {
415+
return NSLocalizedString("TAP THE GREEN BUTTON TO GET STARTED", comment: "")
416+
}
417+
} else {
418+
return NSLocalizedString("SEARCHING FOR NEARBY DEVICES...", comment: "")
419+
}
194420
}
195421
}
196422

197423
extension DeviceScannerViewController: MCNearbyServiceBrowserDelegate {
198424
public func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
199425
connectedPeers.append(peerID)
200-
tableView.reloadData()
426+
427+
// Enable speed run scanning for future visits - user has successfully found a peer
428+
speedRunScanning = true
429+
430+
DispatchQueue.main.async {
431+
self.tableView.reloadData()
432+
}
201433
}
202434

203435
public func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
204436
connectedPeers = connectedPeers.filter { (peer) -> Bool in peer != peerID }
205-
tableView.reloadData()
437+
DispatchQueue.main.async {
438+
self.tableView.reloadData()
439+
}
440+
}
441+
442+
public func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) {
443+
print("Browser failed to start browsing: \(error.localizedDescription)")
444+
445+
// Reset speed run mode and set error state when scanning fails
446+
speedRunScanning = false
447+
hasScanningError = true
448+
449+
DispatchQueue.main.async {
450+
self.stopScanning()
451+
452+
// Show error alert to user
453+
let alert = UIAlertController(
454+
title: NSLocalizedString("Scanning Error", comment: ""),
455+
message: NSLocalizedString("Unable to scan for nearby devices. Please check your network settings and try again.", comment: ""),
456+
preferredStyle: .alert
457+
)
458+
459+
alert.addAction(UIAlertAction(
460+
title: NSLocalizedString("OK", comment: ""),
461+
style: .default,
462+
handler: nil
463+
))
464+
465+
self.present(alert, animated: true) {
466+
// Reload table after scanning error alert is presented
467+
DispatchQueue.main.async {
468+
self.tableView.reloadData()
469+
}
470+
}
471+
}
206472
}
207473
}
208474

0 commit comments

Comments
 (0)