move Peered to implementation directory

This commit is contained in:
2026-03-12 11:01:05 +01:00
parent d7497e2614
commit 9ab7e0bdd0
24 changed files with 22 additions and 0 deletions
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,85 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,57 @@
//
// CommunicationProvider.swift
// Peered
//
// Created by Oskar Chybowski on 11/05/2025.
//
import Foundation
import MultipeerConnectivity
@Observable
final class CommunicationProvider: NSObject, MCNearbyServiceBrowserDelegate {
private let session: MCSession
private let browser: MCNearbyServiceBrowser
private let advertiser: MCNearbyServiceAdvertiser
private(set) var availablePeers: [MCPeerID] = []
init(id: String) {
let id = MCPeerID(displayName: id)
session = MCSession(peer: id, securityIdentity: nil, encryptionPreference: .required)
browser = MCNearbyServiceBrowser(peer: id, serviceType: "peered")
advertiser = MCNearbyServiceAdvertiser(
peer: browser.myPeerID,
discoveryInfo: [:],
serviceType: "peered"
)
super.init()
browser.delegate = self
start()
}
func start() {
advertiser.startAdvertisingPeer()
browser.startBrowsingForPeers()
}
func stop() {
browser.stopBrowsingForPeers()
advertiser.stopAdvertisingPeer()
}
func browser(
_ browser: MCNearbyServiceBrowser,
foundPeer peerID: MCPeerID,
withDiscoveryInfo info: [String: String]?
) {
availablePeers.append(peerID)
browser.invitePeer(peerID, to: session, withContext: nil, timeout: 30)
}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
availablePeers.removeAll { id in
id == peerID
}
}
}
+69
View File
@@ -0,0 +1,69 @@
//
// ContentView.swift
// Peered
//
// Created by Oskar Chybowski on 11/05/2025.
//
import SwiftUI
extension EnvironmentValues {
@Entry var ownPeer: OwnPeer = .fallback
}
struct ContentView: View {
@AppStorage("peered_username") private var username: String = "fallback_user"
@State private var notes = [Note]()
@State private var notesClient: NoteEditingSessionClient?
@State private var ownPeer: OwnPeer?
var body: some View {
NavigationStack {
List {
Section("Your notes") {
ForEach(notes) { note in
NavigationLink(note.name) {
let peer = ownPeer ?? .init(peer: .init(displayName: username))
if ownPeer == nil {
ownPeer = peer
}
return NoteEditorScreen(note: note, peer: peer)
}
}
}
if let notesClient {
Section("External notes") {
ForEach(notesClient.invitations) { invitation in
NavigationLink(invitation.noteName) {
SharedNoteEditor(invitation: invitation, noteClient: notesClient)
}
}
}
}
}
.environment(\.ownPeer, ownPeer ?? .fallback)
.navigationTitle("Peered")
.toolbar {
Button("Create note") {
NotesStorage().createNote(name: "New Note")
notes = NotesStorage().loadNotes()
}
}
}
.onAppear {
notes = NotesStorage().loadNotes()
if notesClient == nil {
notesClient = .init(peer: .init(displayName: username))
}
notesClient?.startBrowsingForNotes()
}
.sheet(isPresented: .constant(username == "fallback_user" || username.isEmpty)) {
SetUserNameBottomSheetView(username: $username)
}
}
}
#Preview {
ContentView()
}
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSBonjourServices</key>
<array>
<string>_peered._tcp</string>
</array>
<key>NSServices</key>
<array>
<dict/>
</array>
</dict>
</plist>
@@ -0,0 +1,28 @@
//
// ManageMembersView.swift
// Peered
//
// Created by Oskar Chybowski on 06/10/2025.
//
import SwiftUI
struct ManageMembersView: 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")
}
}
@@ -0,0 +1,124 @@
import MultipeerConnectivity
import Foundation
import Combine
struct OwnPeer {
let peer: MCPeerID
static var fallback: Self { Self(peer: .init(displayName: "fallback_user")) }
}
struct Peer: Identifiable {
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 let ownPeer: OwnPeer
var visiblePeers: [Peer] = []
let noteChangesEmitter = PassthroughSubject<String, Never>()
init(peer: OwnPeer) {
ownPeer = peer
browser = .init(peer: peer.peer, serviceType: "peered")
session = .init(peer: peer.peer)
super.init()
browser.delegate = self
session.delegate = self
}
func startServer() {
browser.startBrowsingForPeers()
}
func stopServer() {
browser.stopBrowsingForPeers()
}
func invite(peer: Peer, to note: NoteInvitation.NoteContent) {
guard peer.state == .available else { return }
browser.invitePeer(
peer.mcPeer,
to: session,
withContext: try! JSONEncoder().encode(note),
timeout: 5
)
let idxToUpdate = visiblePeers.firstIndex(where: { $0.mcPeer == peer.mcPeer })!
visiblePeers[idxToUpdate].state = .invitationPending
}
}
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 }
let newPeer = Peer(mcPeer: peerID, state: .available)
visiblePeers.append(newPeer)
}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
guard let peerIdx = visiblePeers.firstIndex(where: { $0.mcPeer == peerID }) else { return }
visiblePeers.remove(at: peerIdx)
}
}
extension NoteEditingSessionServer: MCSessionDelegate {
func session(
_ session: MCSession,
peer peerID: MCPeerID,
didChange state: MCSessionState
) {
}
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, didReceive data: Data, fromPeer peerID: MCPeerID) {
guard let note = String(data: data, encoding: .utf8) else { fatalError() }
noteChangesEmitter.send(note)
}
}
@@ -0,0 +1,61 @@
//
// NoteEditorScreen.swift
// Peered
//
// Created by Oskar Chybowski on 25/09/2025.
//
import SwiftUI
import MultipeerConnectivity
struct NoteEditorScreen: View {
let note: Note
@State private var noteAdvertiser: NoteEditingSessionServer
@State private var noteContent: String = ""
@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))
}
var body: some View {
TextEditor(text: $noteContent)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Manage members") {
showManageMembers = true
}
}
}
.sheet(isPresented: $showManageMembers) {
NavigationStack {
ManageMembersView(
noteAdvertiser: noteAdvertiser,
noteTitle: note.name,
noteContent: $noteContent
)
}
}
.onReceive(noteAdvertiser.noteChangesEmitter) { updatedNote in
self.noteContent = updatedNote
}
.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)
}
}
@@ -0,0 +1,26 @@
//
// PeerStateButton.swift
// Peered
//
// Created by Oskar Chybowski on 06/10/2025.
//
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")
}
}
}
@@ -0,0 +1,48 @@
//
// SharedNoteEditor.swift
// Peered
//
// Created by Oskar Chybowski on 07/10/2025.
//
import SwiftUI
struct SharedNoteEditor: View {
@State var note: String?
@State var sendTask: Task<Void, Never>?
@State var invitation: NoteInvitation
@Bindable var noteClient: NoteEditingSessionClient
init(
invitation: NoteInvitation,
noteClient: NoteEditingSessionClient
) {
self._invitation = .init(initialValue: invitation)
self._noteClient = .init(noteClient)
}
var body: some View {
ZStack {
if let note = Binding($note) {
TextEditor(text: note)
} else {
ProgressView {
Text("Fetching note...")
}
}
}
.onChange(of: note) { _, newValue in
sendTask?.cancel()
sendTask = Task {
try? await Task.sleep(nanoseconds: 500000000)
guard let note else { return }
DispatchQueue.main.async {
noteClient.send(note: note, to: invitation.invitatorID)
}
}
}
.onAppear {
invitation.accept()
note = invitation.note.noteSnapshot
}
}
}
@@ -0,0 +1,15 @@
//
// Note.swift
// Peered
//
// Created by Oskar Chybowski on 25/09/2025.
//
import Foundation
struct Note: Identifiable {
var id: URL { path }
let name: String
let path: URL
}
@@ -0,0 +1,113 @@
import MultipeerConnectivity
import Foundation
struct NoteInvitation: Identifiable {
struct NoteContent: Codable {
let title: String
let noteSnapshot: String
}
var id: MCPeerID { invitatorID }
let noteName: String
let invitatorID: MCPeerID
let note: NoteContent
private var invitationHandler: ((Bool) -> Void)?
init(
noteName: String,
invitatorID: MCPeerID,
note: NoteContent,
invitationHandler: ((Bool) -> Void)? = nil
) {
self.noteName = noteName
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 let ownPeer: MCPeerID
var invitations: [NoteInvitation] = [] {
didSet {
print(invitations)
}
}
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
}
func startBrowsingForNotes() {
advertiser.startAdvertisingPeer()
}
func stopBrowsingForNotes() {
advertiser.stopAdvertisingPeer()
}
func send(note: String, to peer: MCPeerID) {
try! session.send(
note.data(using: .utf8)!,
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 { fatalError() }
invitations.append(
.init(
noteName: noteContent.title,
invitatorID: peerID,
note: noteContent,
invitationHandler: { [weak self, invitationHandler] accepted in
guard let self else { return }
invitationHandler(accepted, self.session)
DispatchQueue.main.async {
guard let idx = self.invitations.firstIndex(where: { $0.id == peerID }) else { return }
self.invitations.remove(at: idx)
}
}
)
)
}
}
@@ -0,0 +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)
}
}
}
@@ -0,0 +1,58 @@
//
// NotesStorage.swift
// Peered
//
// Created by Oskar Chybowski on 25/09/2025.
//
import Foundation
struct NotesStorage {
let storageProvider: FileManager = FileManager.default
func loadNotes() -> [Note] {
let files = try! storageProvider
.contentsOfDirectory(atPath: URL.documentsDirectory.path)
var notes = [Note]()
for file in files.compactMap({
URL(
filePath: $0,
directoryHint: .notDirectory,
relativeTo: URL.documentsDirectory
)
}) {
let name = file.lastPathComponent
let note = Note(
name: name,
path: file
)
notes.append(note)
}
return notes
}
func createNote(name: String) {
let currentNotes = loadNotes()
var proposedName = name
var index: Int? = nil
while currentNotes.contains(where: { $0.name == proposedName }) {
if let _index = index {
index = _index + 1
} else {
index = 1
}
}
proposedName = if let index {
"\(proposedName) \(index)"
} else {
proposedName
}
let pathToWrite = URL.documentsDirectory.appendingPathComponent(proposedName).appendingPathExtension(for: .text)
try! String().write(to: pathToWrite, atomically: true, encoding: .utf8)
}
}
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
+17
View File
@@ -0,0 +1,17 @@
//
// PeeredApp.swift
// Peered
//
// Created by Oskar Chybowski on 11/05/2025.
//
import SwiftUI
@main
struct PeeredApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@@ -0,0 +1,19 @@
import SwiftUI
struct SetUserNameBottomSheetView: View {
@State private var proposedUsername: String = ""
@Binding var username: String
var body: some View {
Form {
Section("Set your username") {
TextField("Username", text: $proposedUsername)
}
Button("Save username") {
username = proposedUsername
}
}
.interactiveDismissDisabled()
}
}