From 40e840ee502703b040238516309e56ad833aa098 Mon Sep 17 00:00:00 2001 From: Oschly Date: Sun, 24 May 2026 20:40:56 +0200 Subject: [PATCH] format code --- Implementation/Peered/AllNotesScreen.swift | 126 ++++----- .../NoteEditor/ManageMembersScreen.swift | 36 +-- .../NoteEditor/NoteEditingSessionServer.swift | 234 +++++++++-------- .../Peered/NoteEditor/NoteEditorScreen.swift | 118 ++++----- .../Peered/NoteEditor/NoteTextEditor.swift | 24 +- .../Peered/NoteEditor/PeerStateButton.swift | 30 +-- .../Peered/NoteEditor/SharedNoteEditor.swift | 70 ++--- Implementation/Peered/NotesList/Note.swift | 6 +- .../NotesList/NoteEditingSessionClient.swift | 241 +++++++++--------- .../Peered/NotesList/NoteInvitationView.swift | 20 +- .../Peered/NotesList/NoteMessage.swift | 4 +- .../Peered/NotesList/NotesStorage.swift | 114 +++++---- 12 files changed, 526 insertions(+), 497 deletions(-) diff --git a/Implementation/Peered/AllNotesScreen.swift b/Implementation/Peered/AllNotesScreen.swift index d6ff478..cfafb68 100644 --- a/Implementation/Peered/AllNotesScreen.swift +++ b/Implementation/Peered/AllNotesScreen.swift @@ -8,70 +8,70 @@ import SwiftUI struct AllNotesScreen: View { - @AppStorage("peered_username") private var username: String? - @State private var notes = [Note]() - @State private var notesClient: NoteEditingSessionClient? - @State private var ownPeer: OwnPeer? - - var isUsernameValid: Bool { - !(username.map(\.isEmpty) ?? true) - } - - var body: some View { - NavigationStack { - List { - Section("Your notes") { - ForEach(notes) { note in - NavigationLink(note.name) { - if let ownPeer { - NoteEditorScreen(note: note, peer: ownPeer) - } - } - } - } - - if let notesClient { - Section("External notes") { - ForEach(notesClient.invitations) { invitation in - NavigationLink(invitation.noteName) { - SharedNoteEditor(invitation: invitation, noteClient: notesClient) - } - } - } - } - } - .navigationTitle("Peered") - .toolbar { - Button("Create note") { - NotesStorage().createNote(name: "New Note") - notes = NotesStorage().loadNotes() - } - } - } - .onAppear { - notes = NotesStorage().loadNotes() - if let username, isUsernameValid { - setupSession(username: username) - } - } - .onChange(of: username) { _, newUsername in - guard let newUsername, isUsernameValid else { return } - setupSession(username: newUsername) - } - .sheet(isPresented: .constant(!isUsernameValid)) { - SetUserNameBottomSheetView(username: $username) - } - } - - private func setupSession(username: String) { - notesClient?.stopBrowsingForNotes() - let peer = OwnPeer(peer: .init(displayName: username)) - ownPeer = peer - notesClient = .init(peer: peer.peer) - notesClient?.startBrowsingForNotes() - } + @AppStorage("peered_username") private var username: String? + @State private var notes = [Note]() + @State private var notesClient: NoteEditingSessionClient? + @State private var ownPeer: OwnPeer? + + var isUsernameValid: Bool { + !(username.map(\.isEmpty) ?? true) + } + + var body: some View { + NavigationStack { + List { + Section("Your notes") { + ForEach(notes) { note in + NavigationLink(note.name) { + if let ownPeer { + NoteEditorScreen(note: note, peer: ownPeer) + } + } + } + } + + if let notesClient { + Section("External notes") { + ForEach(notesClient.invitations) { invitation in + NavigationLink(invitation.noteName) { + SharedNoteEditor(invitation: invitation, noteClient: notesClient) + } + } + } + } + } + .navigationTitle("Peered") + .toolbar { + Button("Create note") { + NotesStorage().createNote(name: "New Note") + notes = NotesStorage().loadNotes() + } + } + } + .onAppear { + notes = NotesStorage().loadNotes() + if let username, isUsernameValid { + setupSession(username: username) + } + } + .onChange(of: username) { _, newUsername in + guard let newUsername, isUsernameValid else { return } + setupSession(username: newUsername) + } + .sheet(isPresented: .constant(!isUsernameValid)) { + SetUserNameBottomSheetView(username: $username) + } + } + + private func setupSession(username: String) { + notesClient?.stopBrowsingForNotes() + let peer = OwnPeer(peer: .init(displayName: username)) + ownPeer = peer + notesClient = .init(peer: peer.peer) + notesClient?.startBrowsingForNotes() + } } #Preview { - AllNotesScreen() + AllNotesScreen() } diff --git a/Implementation/Peered/NoteEditor/ManageMembersScreen.swift b/Implementation/Peered/NoteEditor/ManageMembersScreen.swift index 907e669..10d988d 100644 --- a/Implementation/Peered/NoteEditor/ManageMembersScreen.swift +++ b/Implementation/Peered/NoteEditor/ManageMembersScreen.swift @@ -7,22 +7,22 @@ import SwiftUI struct ManageMembersScreen: View { - @Bindable var noteAdvertiser: NoteEditingSessionServer - let noteTitle: String - @Binding var noteContent: String - - var body: some View { - List(noteAdvertiser.visiblePeers) { peer in - HStack { - Text(peer.id) - - Spacer() - - PeerStateButton(peerState: peer.state) { - noteAdvertiser.invite(peer: peer, to: .init(title: noteTitle, noteSnapshot: noteContent)) - } - } - } - .navigationTitle("Visible users") - } + @Bindable var noteAdvertiser: NoteEditingSessionServer + let noteTitle: String + @Binding var noteContent: String + + var body: some View { + List(noteAdvertiser.visiblePeers) { peer in + HStack { + Text(peer.id) + + Spacer() + + PeerStateButton(peerState: peer.state) { + noteAdvertiser.invite(peer: peer, to: .init(title: noteTitle, noteSnapshot: noteContent)) + } + } + } + .navigationTitle("Visible users") + } } diff --git a/Implementation/Peered/NoteEditor/NoteEditingSessionServer.swift b/Implementation/Peered/NoteEditor/NoteEditingSessionServer.swift index cfca354..2f3dddc 100644 --- a/Implementation/Peered/NoteEditor/NoteEditingSessionServer.swift +++ b/Implementation/Peered/NoteEditor/NoteEditingSessionServer.swift @@ -1,126 +1,142 @@ -import MultipeerConnectivity -import Foundation import Combine +import Foundation +import MultipeerConnectivity struct OwnPeer { - let peer: MCPeerID + let peer: MCPeerID } struct Peer: Identifiable { - enum ConnectionState { - case available - case joined - case rejected - case invitationPending - } - - var id: String { mcPeer.displayName } - let mcPeer: MCPeerID - var state: ConnectionState + enum ConnectionState { + case available + case joined + case rejected + case invitationPending + } + + var id: String { mcPeer.displayName } + let mcPeer: MCPeerID + var state: ConnectionState } @Observable final class NoteEditingSessionServer: NSObject { - private let session: MCSession - private let browser: MCNearbyServiceBrowser - private(set) var ownPeer: OwnPeer - - var visiblePeers: [Peer] = [] - let noteChangesEmitter = PassthroughSubject() - - init(peer: OwnPeer) { - ownPeer = peer - browser = .init(peer: peer.peer, serviceType: "peered") - session = .init(peer: peer.peer, securityIdentity: nil, encryptionPreference: .required) - super.init() - browser.delegate = self - session.delegate = self - } - - func startServer() { - browser.startBrowsingForPeers() - } - - func stopServer() { - browser.stopBrowsingForPeers() - session.disconnect() - } - - func invite(peer: Peer, to note: NoteInvitation.NoteContent) { - guard peer.state == .available, let note = try? JSONEncoder().encode(note) else { return } - browser.invitePeer( - peer.mcPeer, - to: session, - withContext: note, - timeout: 600 - ) - guard let idxToUpdate = visiblePeers.firstIndex(where: { $0.mcPeer == peer.mcPeer }) else { return } - visiblePeers[idxToUpdate].state = .invitationPending - } - - func send(note: String, to peers: [MCPeerID]) { - let message = NoteMessage(senderID: ownPeer.peer.displayName, content: note) - guard !peers.isEmpty, let data = try? JSONEncoder().encode(message) else { return } - try? session.send(data, toPeers: peers, with: .reliable) - } + private let session: MCSession + private let browser: MCNearbyServiceBrowser + private(set) var ownPeer: OwnPeer + + var visiblePeers: [Peer] = [] + let noteChangesEmitter = PassthroughSubject() + + init(peer: OwnPeer) { + ownPeer = peer + browser = .init(peer: peer.peer, serviceType: "peered") + session = .init(peer: peer.peer, securityIdentity: nil, encryptionPreference: .required) + super.init() + browser.delegate = self + session.delegate = self + } + + func startServer() { + browser.startBrowsingForPeers() + } + + func stopServer() { + browser.stopBrowsingForPeers() + session.disconnect() + } + + func invite(peer: Peer, to note: NoteInvitation.NoteContent) { + guard peer.state == .available, let note = try? JSONEncoder().encode(note) else { return } + browser.invitePeer( + peer.mcPeer, + to: session, + withContext: note, + timeout: 600 + ) + guard let idxToUpdate = visiblePeers.firstIndex(where: { $0.mcPeer == peer.mcPeer }) else { + return + } + visiblePeers[idxToUpdate].state = .invitationPending + } + + func send(note: String, to peers: [MCPeerID]) { + let message = NoteMessage(senderID: ownPeer.peer.displayName, content: note) + guard !peers.isEmpty, let data = try? JSONEncoder().encode(message) else { return } + try? session.send(data, toPeers: peers, with: .reliable) + } } extension NoteEditingSessionServer: MCNearbyServiceBrowserDelegate { - func browser( - _ browser: MCNearbyServiceBrowser, - foundPeer peerID: MCPeerID, - withDiscoveryInfo info: [String: String]? - ) { - guard !visiblePeers.contains(where: { $0.mcPeer == peerID }) && peerID.displayName != ownPeer.peer.displayName else { return } - DispatchQueue.main.async { - self.visiblePeers.append(Peer(mcPeer: peerID, state: .available)) - } - } - - func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { - DispatchQueue.main.async { - guard let peerIdx = self.visiblePeers.firstIndex(where: { $0.mcPeer == peerID }) else { return } - self.visiblePeers.remove(at: peerIdx) - } - } + func browser( + _ browser: MCNearbyServiceBrowser, + foundPeer peerID: MCPeerID, + withDiscoveryInfo info: [String: String]? + ) { + guard + !visiblePeers.contains(where: { $0.mcPeer == peerID }) + && peerID.displayName != ownPeer.peer.displayName + else { return } + DispatchQueue.main.async { + self.visiblePeers.append(Peer(mcPeer: peerID, state: .available)) + } + } + + func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { + DispatchQueue.main.async { + guard let peerIdx = self.visiblePeers.firstIndex(where: { $0.mcPeer == peerID }) else { + return + } + self.visiblePeers.remove(at: peerIdx) + } + } } extension NoteEditingSessionServer: MCSessionDelegate { - func session( - _ session: MCSession, - peer peerID: MCPeerID, - didChange state: MCSessionState - ) { - DispatchQueue.main.async { - guard let idx = self.visiblePeers.firstIndex(where: { $0.mcPeer == peerID }) else { return } - switch state { - case .connected: - self.visiblePeers[idx].state = .joined - case .notConnected: - let currentState = self.visiblePeers[idx].state - if currentState == .invitationPending || currentState == .joined { - self.visiblePeers[idx].state = .rejected - } - default: - break - } - } - } - - func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { - guard let message = try? JSONDecoder().decode(NoteMessage.self, from: data) else { return } - - let otherPeers = session.connectedPeers.filter { $0 != peerID } - if !otherPeers.isEmpty { - try? session.send(data, toPeers: otherPeers, with: .reliable) - } - - DispatchQueue.main.async { - self.noteChangesEmitter.send(message) - } - } - - func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {} - func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {} - func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: (any Error)?) {} + func session( + _ session: MCSession, + peer peerID: MCPeerID, + didChange state: MCSessionState + ) { + DispatchQueue.main.async { + guard let idx = self.visiblePeers.firstIndex(where: { $0.mcPeer == peerID }) else { return } + switch state { + case .connected: + self.visiblePeers[idx].state = .joined + case .notConnected: + let currentState = self.visiblePeers[idx].state + if currentState == .invitationPending || currentState == .joined { + self.visiblePeers[idx].state = .rejected + } + default: + break + } + } + } + + func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { + guard let message = try? JSONDecoder().decode(NoteMessage.self, from: data) else { return } + + let otherPeers = session.connectedPeers.filter { $0 != peerID } + if !otherPeers.isEmpty { + try? session.send(data, toPeers: otherPeers, with: .reliable) + } + + DispatchQueue.main.async { + self.noteChangesEmitter.send(message) + } + } + + func session( + _ session: MCSession, didReceive stream: InputStream, withName streamName: String, + fromPeer peerID: MCPeerID + ) {} + func session( + _ session: MCSession, didStartReceivingResourceWithName resourceName: String, + fromPeer peerID: MCPeerID, with progress: Progress + ) {} + func session( + _ session: MCSession, didFinishReceivingResourceWithName resourceName: String, + fromPeer peerID: MCPeerID, at localURL: URL?, withError error: (any Error)? + ) {} } diff --git a/Implementation/Peered/NoteEditor/NoteEditorScreen.swift b/Implementation/Peered/NoteEditor/NoteEditorScreen.swift index f28d602..2555a42 100644 --- a/Implementation/Peered/NoteEditor/NoteEditorScreen.swift +++ b/Implementation/Peered/NoteEditor/NoteEditorScreen.swift @@ -5,69 +5,69 @@ // Created by Oskar Chybowski on 25/09/2025. // -import SwiftUI import MultipeerConnectivity +import SwiftUI struct NoteEditorScreen: View { - let note: Note - @State private var noteAdvertiser: NoteEditingSessionServer - @State private var noteContent: String = "" - @State private var remoteNoteContent: String? = nil - @State private var timer = Timer.publish(every: 5, on: .current, in: .common) - .autoconnect() - @State private var showManageMembers = false + let note: Note + @State private var noteAdvertiser: NoteEditingSessionServer + @State private var noteContent: String = "" + @State private var remoteNoteContent: String? = nil + @State private var timer = Timer.publish(every: 5, on: .current, in: .common) + .autoconnect() + @State private var showManageMembers = false - init(note: Note, peer: OwnPeer) { - self.note = note - self._noteAdvertiser = .init(initialValue: .init(peer: peer)) - } + init(note: Note, peer: OwnPeer) { + self.note = note + self._noteAdvertiser = .init(initialValue: .init(peer: peer)) + } - var body: some View { - NoteTextEditor(text: $noteContent, remoteText: remoteNoteContent) - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button("Manage members") { - showManageMembers = true - } - } - } - .sheet(isPresented: $showManageMembers) { - NavigationStack { - ManageMembersScreen( - noteAdvertiser: noteAdvertiser, - noteTitle: note.name, - noteContent: $noteContent - ) - } - } - .onReceive(noteAdvertiser.noteChangesEmitter) { message in - guard message.senderID != noteAdvertiser.ownPeer.peer.displayName else { return } - remoteNoteContent = message.content - } - .task(id: noteContent) { - if noteContent == remoteNoteContent { return } - let connectedPeers = noteAdvertiser.visiblePeers - .filter { $0.state == .joined } - .map(\.mcPeer) - guard !connectedPeers.isEmpty else { return } - try? await Task.sleep(nanoseconds: 500_000_000) - guard !Task.isCancelled else { return } - noteAdvertiser.send(note: noteContent, to: connectedPeers) - } - .onAppear { - noteContent = (try? String(contentsOf: note.path, encoding: .utf8)) ?? "" - noteAdvertiser.startServer() - } - .onDisappear { - noteAdvertiser.stopServer() - saveNote() - } - .onReceive(timer) { _ in - saveNote() - } - } + var body: some View { + NoteTextEditor(text: $noteContent, remoteText: remoteNoteContent) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button("Manage members") { + showManageMembers = true + } + } + } + .sheet(isPresented: $showManageMembers) { + NavigationStack { + ManageMembersScreen( + noteAdvertiser: noteAdvertiser, + noteTitle: note.name, + noteContent: $noteContent + ) + } + } + .onReceive(noteAdvertiser.noteChangesEmitter) { message in + guard message.senderID != noteAdvertiser.ownPeer.peer.displayName else { return } + remoteNoteContent = message.content + } + .task(id: noteContent) { + if noteContent == remoteNoteContent { return } + let connectedPeers = noteAdvertiser.visiblePeers + .filter { $0.state == .joined } + .map(\.mcPeer) + guard !connectedPeers.isEmpty else { return } + try? await Task.sleep(nanoseconds: 500_000_000) + guard !Task.isCancelled else { return } + noteAdvertiser.send(note: noteContent, to: connectedPeers) + } + .onAppear { + noteContent = (try? String(contentsOf: note.path, encoding: .utf8)) ?? "" + noteAdvertiser.startServer() + } + .onDisappear { + noteAdvertiser.stopServer() + saveNote() + } + .onReceive(timer) { _ in + saveNote() + } + } - func saveNote() { - try? noteContent.write(to: note.path, atomically: true, encoding: .utf8) - } + func saveNote() { + try? noteContent.write(to: note.path, atomically: true, encoding: .utf8) + } } diff --git a/Implementation/Peered/NoteEditor/NoteTextEditor.swift b/Implementation/Peered/NoteEditor/NoteTextEditor.swift index 9380383..6d40a79 100644 --- a/Implementation/Peered/NoteEditor/NoteTextEditor.swift +++ b/Implementation/Peered/NoteEditor/NoteTextEditor.swift @@ -1,16 +1,16 @@ import SwiftUI struct NoteTextEditor: View { - @Binding var text: String - let remoteText: String? - @State private var selection: TextSelection? = nil - - var body: some View { - TextEditor(text: $text, selection: $selection) - .onChange(of: remoteText) { oldValue, newValue in - guard let newValue, newValue != text else { return } - // Apply remote text first - text = newValue - } - } + @Binding var text: String + let remoteText: String? + @State private var selection: TextSelection? = nil + + var body: some View { + TextEditor(text: $text, selection: $selection) + .onChange(of: remoteText) { oldValue, newValue in + guard let newValue, newValue != text else { return } + // Apply remote text first + text = newValue + } + } } diff --git a/Implementation/Peered/NoteEditor/PeerStateButton.swift b/Implementation/Peered/NoteEditor/PeerStateButton.swift index fb3482d..62c1251 100644 --- a/Implementation/Peered/NoteEditor/PeerStateButton.swift +++ b/Implementation/Peered/NoteEditor/PeerStateButton.swift @@ -8,19 +8,19 @@ import SwiftUI struct PeerStateButton: View { - let peerState: Peer.ConnectionState - let onTap: () -> Void - - var body: some View { - switch peerState { - case .available: - Button("Invite", action: onTap) - case .joined: - Text("Joined") - case .rejected: - Text("Rejected") - case .invitationPending: - Text("Invitation pending") - } - } + let peerState: Peer.ConnectionState + let onTap: () -> Void + + var body: some View { + switch peerState { + case .available: + Button("Invite", action: onTap) + case .joined: + Text("Joined") + case .rejected: + Text("Rejected") + case .invitationPending: + Text("Invitation pending") + } + } } diff --git a/Implementation/Peered/NoteEditor/SharedNoteEditor.swift b/Implementation/Peered/NoteEditor/SharedNoteEditor.swift index 5c6e0e6..f2d9ebc 100644 --- a/Implementation/Peered/NoteEditor/SharedNoteEditor.swift +++ b/Implementation/Peered/NoteEditor/SharedNoteEditor.swift @@ -7,41 +7,41 @@ import SwiftUI struct SharedNoteEditor: View { - @State var note: String? - @State var remoteNote: String? = nil - @State var invitation: NoteInvitation - @Bindable var noteClient: NoteEditingSessionClient + @State var note: String? + @State var remoteNote: String? = nil + @State var invitation: NoteInvitation + @Bindable var noteClient: NoteEditingSessionClient - init( - invitation: NoteInvitation, - noteClient: NoteEditingSessionClient - ) { - self._invitation = .init(initialValue: invitation) - self._noteClient = .init(noteClient) - } + init( + invitation: NoteInvitation, + noteClient: NoteEditingSessionClient + ) { + self._invitation = .init(initialValue: invitation) + self._noteClient = .init(noteClient) + } - var body: some View { - ZStack { - if let note = Binding($note) { - NoteTextEditor(text: note, remoteText: remoteNote) - } else { - ProgressView { - Text("Fetching note...") - } - } - } - .onReceive(noteClient.noteChangesEmitter) { message in - guard message.senderID != noteClient.ownPeer.displayName else { return } - remoteNote = message.content - } - .task(id: note) { - try? await Task.sleep(nanoseconds: 500_000_000) - guard !Task.isCancelled, let note else { return } - noteClient.send(note: note, to: invitation.invitatorID) - } - .onAppear { - invitation.accept() - note = invitation.note.noteSnapshot - } - } + var body: some View { + ZStack { + if let note = Binding($note) { + NoteTextEditor(text: note, remoteText: remoteNote) + } else { + ProgressView { + Text("Fetching note...") + } + } + } + .onReceive(noteClient.noteChangesEmitter) { message in + guard message.senderID != noteClient.ownPeer.displayName else { return } + remoteNote = message.content + } + .task(id: note) { + try? await Task.sleep(nanoseconds: 500_000_000) + guard !Task.isCancelled, let note else { return } + noteClient.send(note: note, to: invitation.invitatorID) + } + .onAppear { + invitation.accept() + note = invitation.note.noteSnapshot + } + } } diff --git a/Implementation/Peered/NotesList/Note.swift b/Implementation/Peered/NotesList/Note.swift index 73e2788..401a4e9 100644 --- a/Implementation/Peered/NotesList/Note.swift +++ b/Implementation/Peered/NotesList/Note.swift @@ -8,8 +8,8 @@ import Foundation struct Note: Identifiable { - var id: URL { path } + var id: URL { path } - let name: String - let path: URL + let name: String + let path: URL } diff --git a/Implementation/Peered/NotesList/NoteEditingSessionClient.swift b/Implementation/Peered/NotesList/NoteEditingSessionClient.swift index 571b92f..3a6cea9 100644 --- a/Implementation/Peered/NotesList/NoteEditingSessionClient.swift +++ b/Implementation/Peered/NotesList/NoteEditingSessionClient.swift @@ -1,131 +1,140 @@ -import MultipeerConnectivity -import Foundation import Combine +import Foundation +import MultipeerConnectivity struct NoteInvitation: Identifiable { - struct NoteContent: Codable { - let title: String - let noteSnapshot: String - } - - var id: MCPeerID { invitatorID } - var noteName: String { note.title } - let invitatorID: MCPeerID - let note: NoteContent - private var invitationHandler: ((Bool) -> Void)? - - init( - invitatorID: MCPeerID, - note: NoteContent, - invitationHandler: ((Bool) -> Void)? = nil - ) { - self.invitatorID = invitatorID - self.note = note - self.invitationHandler = invitationHandler - } - - mutating func accept() { - invitationHandler?(true) - invitationHandler = nil - } - - mutating func decline() { - invitationHandler?(false) - invitationHandler = nil - } + struct NoteContent: Codable { + let title: String + let noteSnapshot: String + } + + var id: MCPeerID { invitatorID } + var noteName: String { note.title } + let invitatorID: MCPeerID + let note: NoteContent + private var invitationHandler: ((Bool) -> Void)? + + init( + invitatorID: MCPeerID, + note: NoteContent, + invitationHandler: ((Bool) -> Void)? = nil + ) { + self.invitatorID = invitatorID + self.note = note + self.invitationHandler = invitationHandler + } + + mutating func accept() { + invitationHandler?(true) + invitationHandler = nil + } + + mutating func decline() { + invitationHandler?(false) + invitationHandler = nil + } } @Observable final class NoteEditingSessionClient: NSObject { - private let session: MCSession - private let advertiser: MCNearbyServiceAdvertiser - private(set) var ownPeer: MCPeerID - - var invitations: [NoteInvitation] = [] - let noteChangesEmitter = PassthroughSubject() + private let session: MCSession + private let advertiser: MCNearbyServiceAdvertiser + private(set) var ownPeer: MCPeerID - init(peer: MCPeerID) { - ownPeer = peer - session = MCSession( - peer: peer, - securityIdentity: nil, - encryptionPreference: .required - ) - advertiser = MCNearbyServiceAdvertiser( - peer: peer, - discoveryInfo: [:], - serviceType: "peered" - ) - super.init() - advertiser.delegate = self - session.delegate = self - } - - func startBrowsingForNotes() { - advertiser.startAdvertisingPeer() - } - - func stopBrowsingForNotes() { - advertiser.stopAdvertisingPeer() - session.disconnect() - } - - func send(note: String, to peer: MCPeerID) { - let message = NoteMessage(senderID: ownPeer.displayName, content: note) - guard let data = try? JSONEncoder().encode(message) else { return } - try? session.send(data, toPeers: [peer], with: .reliable) - } + var invitations: [NoteInvitation] = [] + let noteChangesEmitter = PassthroughSubject() + + init(peer: MCPeerID) { + ownPeer = peer + session = MCSession( + peer: peer, + securityIdentity: nil, + encryptionPreference: .required + ) + advertiser = MCNearbyServiceAdvertiser( + peer: peer, + discoveryInfo: [:], + serviceType: "peered" + ) + super.init() + advertiser.delegate = self + session.delegate = self + } + + func startBrowsingForNotes() { + advertiser.startAdvertisingPeer() + } + + func stopBrowsingForNotes() { + advertiser.stopAdvertisingPeer() + session.disconnect() + } + + func send(note: String, to peer: MCPeerID) { + let message = NoteMessage(senderID: ownPeer.displayName, content: note) + guard let data = try? JSONEncoder().encode(message) else { return } + try? session.send(data, toPeers: [peer], with: .reliable) + } } extension NoteEditingSessionClient: MCNearbyServiceAdvertiserDelegate { - func advertiser( - _ advertiser: MCNearbyServiceAdvertiser, - didReceiveInvitationFromPeer peerID: MCPeerID, - withContext context: Data?, - invitationHandler: @escaping (Bool, MCSession?) -> Void - ) { - guard - let context, - let noteContent = try? JSONDecoder().decode(NoteInvitation.NoteContent.self, from: context) - else { return } - - DispatchQueue.main.async { - self.invitations.append( - .init( - invitatorID: peerID, - note: noteContent, - invitationHandler: { [weak self, invitationHandler] accepted in - guard let self else { return } - invitationHandler(accepted, self.session) - DispatchQueue.main.async { - self.invitations.removeAll { $0.id == peerID } - } - } - ) - ) - } - } + func advertiser( + _ advertiser: MCNearbyServiceAdvertiser, + didReceiveInvitationFromPeer peerID: MCPeerID, + withContext context: Data?, + invitationHandler: @escaping (Bool, MCSession?) -> Void + ) { + guard + let context, + let noteContent = try? JSONDecoder().decode(NoteInvitation.NoteContent.self, from: context) + else { return } + + DispatchQueue.main.async { + self.invitations.append( + .init( + invitatorID: peerID, + note: noteContent, + invitationHandler: { [weak self, invitationHandler] accepted in + guard let self else { return } + invitationHandler(accepted, self.session) + DispatchQueue.main.async { + self.invitations.removeAll { $0.id == peerID } + } + } + ) + ) + } + } } extension NoteEditingSessionClient: MCSessionDelegate { - func session( - _ session: MCSession, - peer peerID: MCPeerID, - didChange state: MCSessionState - ) {} - - func session( - _ session: MCSession, - didReceive data: Data, - fromPeer peerID: MCPeerID - ) { - guard let message = try? JSONDecoder().decode(NoteMessage.self, from: data) else { return } - DispatchQueue.main.async { - self.noteChangesEmitter.send(message) - } - } - - func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {} - func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {} - func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: (any Error)?) {} + func session( + _ session: MCSession, + peer peerID: MCPeerID, + didChange state: MCSessionState + ) {} + + func session( + _ session: MCSession, + didReceive data: Data, + fromPeer peerID: MCPeerID + ) { + guard let message = try? JSONDecoder().decode(NoteMessage.self, from: data) else { return } + DispatchQueue.main.async { + self.noteChangesEmitter.send(message) + } + } + + func session( + _ session: MCSession, didReceive stream: InputStream, withName streamName: String, + fromPeer peerID: MCPeerID + ) {} + func session( + _ session: MCSession, didStartReceivingResourceWithName resourceName: String, + fromPeer peerID: MCPeerID, with progress: Progress + ) {} + func session( + _ session: MCSession, didFinishReceivingResourceWithName resourceName: String, + fromPeer peerID: MCPeerID, at localURL: URL?, withError error: (any Error)? + ) {} } diff --git a/Implementation/Peered/NotesList/NoteInvitationView.swift b/Implementation/Peered/NotesList/NoteInvitationView.swift index f210ee9..1f121b5 100644 --- a/Implementation/Peered/NotesList/NoteInvitationView.swift +++ b/Implementation/Peered/NotesList/NoteInvitationView.swift @@ -1,14 +1,14 @@ import SwiftUI struct NoteInvitationView: View { - @Binding var invitation: NoteInvitation - let joinTapped: () -> Void - - var body: some View { - HStack { - Text(invitation.noteName) - Spacer() - Button("Join", action: joinTapped) - } - } + @Binding var invitation: NoteInvitation + let joinTapped: () -> Void + + var body: some View { + HStack { + Text(invitation.noteName) + Spacer() + Button("Join", action: joinTapped) + } + } } diff --git a/Implementation/Peered/NotesList/NoteMessage.swift b/Implementation/Peered/NotesList/NoteMessage.swift index b0e6625..06b86fc 100644 --- a/Implementation/Peered/NotesList/NoteMessage.swift +++ b/Implementation/Peered/NotesList/NoteMessage.swift @@ -1,4 +1,4 @@ struct NoteMessage: Codable { - let senderID: String - let content: String + let senderID: String + let content: String } diff --git a/Implementation/Peered/NotesList/NotesStorage.swift b/Implementation/Peered/NotesList/NotesStorage.swift index 8ff9772..4907cb4 100644 --- a/Implementation/Peered/NotesList/NotesStorage.swift +++ b/Implementation/Peered/NotesList/NotesStorage.swift @@ -8,72 +8,76 @@ import Foundation protocol StorageProvider { - func contentsOfDirectory(atPath path: String) throws -> [String] - - @discardableResult - func createFile(atPath path: String, contents data: Data?, attributes attr: [FileAttributeKey: Any]?) -> Bool + func contentsOfDirectory(atPath path: String) throws -> [String] + + @discardableResult + func createFile( + atPath path: String, contents data: Data?, attributes attr: [FileAttributeKey: Any]? + ) -> Bool } extension FileManager: StorageProvider {} struct NotesStorage { - let storageProvider: StorageProvider - let rootDirectory: URL + let storageProvider: StorageProvider + let rootDirectory: URL - init( - storageProvider: StorageProvider = FileManager.default, - rootDirectory: URL = .documentsDirectory - ) { - self.storageProvider = storageProvider - self.rootDirectory = rootDirectory - } + init( + storageProvider: StorageProvider = FileManager.default, + rootDirectory: URL = .documentsDirectory + ) { + self.storageProvider = storageProvider + self.rootDirectory = rootDirectory + } - func loadNotes() -> [Note] { - let files = try! storageProvider - .contentsOfDirectory(atPath: rootDirectory.path) - var notes = [Note]() + func loadNotes() -> [Note] { + let files = + try! storageProvider + .contentsOfDirectory(atPath: rootDirectory.path) + var notes = [Note]() - for file in files.compactMap({ - URL( - filePath: $0, - directoryHint: .notDirectory, - relativeTo: rootDirectory - ) - }) { - let name = file.lastPathComponent - let note = Note( - name: name, - path: file - ) - notes.append(note) - } + for file in files.compactMap({ + URL( + filePath: $0, + directoryHint: .notDirectory, + relativeTo: rootDirectory + ) + }) { + let name = file.lastPathComponent + let note = Note( + name: name, + path: file + ) + notes.append(note) + } - return notes - } + return notes + } - func createNote(name: String) { - let currentNotes = loadNotes() - var index: Int? = nil - var proposedName: String { - index.map { name + " \($0)" } ?? name - } + func createNote(name: String) { + let currentNotes = loadNotes() + var index: Int? = nil + var proposedName: String { + index.map { name + " \($0)" } ?? name + } - while currentNotes.contains(where: { $0.name == proposedName }) { - if let _index = index { - index = _index + 1 - } else { - index = 1 - } - } + while currentNotes.contains(where: { $0.name == proposedName }) { + if let _index = index { + index = _index + 1 + } else { + index = 1 + } + } - let pathToWrite = rootDirectory - .appendingPathComponent(proposedName) - .appendingPathExtension(for: .text) + let pathToWrite = + rootDirectory + .appendingPathComponent(proposedName) + .appendingPathExtension(for: .text) - storageProvider.createFile( - atPath: pathToWrite.path, - contents: Data(), - attributes: nil - ) - } + storageProvider.createFile( + atPath: pathToWrite.path, + contents: Data(), + attributes: nil + ) + } }