124 lines
9.6 KiB
Typst
124 lines
9.6 KiB
Typst
#import "../style.typ"
|
||
#set heading(numbering: "1.1")
|
||
|
||
= Implementacja <implementacja>
|
||
== Projekt architektury systemu <projekt>
|
||
Przygotowana implementacja bazuje na architekturze Model-View. Jest to podejście, gdzie cała logika biznesowa jest zawarta w modelach, które bezpośrednio są przekazywane do warstwy prezentacji (View). Na tej warstwie jest wykonywane odpowiednie formatowanie danych, gdzie też brane pod uwagę są preferencje zapisu i językowe użytkownika. Model-View to uproszczony wariant popularnej w aplikacjach mobilnych architektury Model-View-ViewModel (MVVM), gdzie ViewModel jest warstwą zajmującą się przekształcaniem modelów biznesowych na gotowe do prezentacji obiekty. Warstwa View wtedy zajmuje się przede wszystkim definiowaniem struktury interfejsu użytkownika oraz logiką związaną z dostępnością (wsparcie dla funkcjonalności czytników ekranów, skalowaniem interfejsu). Przygotowana implementacja nie zawiera złożonej logiki prezentacji ani rozbudowanego graficznego interfejsu użytkownika, więc w celu uproszczenia kodu zdecydowałem się na porzucenie użycia warstwy ViewModelu.
|
||
|
||
Warstwa prezentacji aplikacji opisująca początkowy interfejs użytkownika jest zaimplementowana w `AllNotesScreen`. Ekran jest widoczny dla użytkownika, gdy ustawił swoją nazwę użytkownika w sieci peer-to-peer. Składa się z listy podzielonej na dwie sekcje - notatek których użytkownik jest autorem, oraz notatek do których użytkownik został zaproszony. Nad listą znajduje się przycisk stworzenia nowej notatki. Naciśnięcie na którąkolwiek z istniejących notatek prowadzi do otwarcia jej zawartości. W zależności od tego, czy użytkownik jest właścicielem danej notatki czy współtwórcą, interfejsem do edycji tej notatki są odpowiednio obiekty `NoteEditorScreen` oraz `SharedNoteEditor`. Z ekranu `NoteEditorScreen` możemy przejść do `ManageMembersScreen`, który jest listą użytkowników zaproszonych i możliwych do zaproszenia do edytowania aktualnej notatki.
|
||
|
||
== Model danych
|
||
Podstawowym obiektem reprezentującym notatkę w systemie plików jest `Note`. Składa się on z URL (Universal Resource Locator), czyli ścieżki prowadzącej do pełnej zawartości notatki użytkownika, która jest przechowywana w pliku tekstowym o rozszerzeniu `txt`. Dodatkowo przechowujemy w tym obiekcie jeszcze `name`, które pełni funkcję ułatwionego dostępu do przyjaznej nazwy, jednocześnie będąc nazwą pliku w lokalnym systemie plików oraz przyjazną nazwą prezentowaną w liście notatek do których użytkownik został zaproszony. Obiekt implementuje protokół `Identifiable`, który gwarantuje możliwość identyfikacji obiektu w zbiorze zawierającym wiele jego instancji, np. w tablicy. Ta cecha jest wymagana i wykorzystywana przez SwiftUI, by móc rozróżniać rodzaje zmian na komponentach interfejsów graficznych zawierających wiele kopii takiego obiektu, np. `List` lub `ForEach`. Umożliwia to dokonanie decyzji co do sposobu animacji zmian na ekranie użytkownika, ponieważ framework będzie mógł rozróżnić usunięcie i wstawienie nowego obiektu, od zmiany parametrów tej samej instancji.
|
||
|
||
```swift
|
||
struct Note: Identifiable {
|
||
var id: URL { path }
|
||
|
||
let name: String
|
||
let path: URL
|
||
}
|
||
```
|
||
|
||
`NoteMessage` to obiekt zawierający zawartość notatki, którą wysyłamy do użytkowników, którzy przyjęli zaproszenie do edycji notatki. Znajdziemy w niej pola `SenderID`, które jest identyfikatorem właściciela notatki oraz `content`, czyli właściwą zawartość notatki. Jest ona również używana do wysyłania każdej aktualizacji do wszystkich użytkowników. Obiekt implementuje protokół `Codable`, który jest uniwersalnym interfejsem kodowania danych. Daje to nam wbudowane wsparcie kodowowania do formatów JSON i XML. Mamy możliwość również pisania własnych koderów, które umożliwią zamianę obiektów implementujących `Codable` do wybranych przez nas, innych formatów danych.
|
||
|
||
```swift
|
||
struct NoteMessage: Codable {
|
||
let senderID: String
|
||
let content: String
|
||
}
|
||
```
|
||
|
||
Do reprezentacji zaproszeń stworzyłem obiekt `NoteInvitation`. Przechowuje on informacje potrzebne do obsługi całego procesu zaproszeń między użytkownikami systemu. Składa się z `invitatorID`, który jest wyspecjalizowanym obiektem frameworku `MultipeerConnectivity` i umożliwia identyfikację użytkownika w czasie komunikacji peer to peer. `note` to obiekt reprezentujący notatkę, którą otrzymujemy w zaproszeniu. Składa się on z tytułu i treści, które były aktualne w momencie zapraszania użytkownika. Ostatnim i jednocześnie prywatnym parametrem jest `invitationHandler`, który jest typem funkcji przyjmującej wartość boolowską (prawda/fałsz). Jest on wykorzystywany do wstrzyknięcia logiki akceptacji notatki, która wymaga kilku operacji w logice klasy obsługującej przyjmowanie zaproszeń. Dzięki takiemu podejściu tworzymy luźną zależność do skomplikowanego obiektu na dalszych etapach systemu. `NoteInvitation` zawiera również metody `accept()` oraz `decline()`, które odpowiadają za wykonanie akcji zaproszenia, które pod spodem odpowiednio używają parametru `invitationHandler`, który można wykonać tylko raz, ze względu na specyfikę `Multipeer Connectivity`, a następnie usuwamy go z pamięci.
|
||
|
||
```swift
|
||
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
|
||
}
|
||
}
|
||
```
|
||
|
||
Ostatnim obiektem jest struktura `Peer`, która jest opisem aktualnego stanu połączenia wykrytego innej instancji systemu w pobliżu użytkownika. Składa się z wartości enumerowanej opisującej stan połączeniao raz identyfikatorem użytkownika w sieci peer to peer. Implementuja ona protokół `Idenfitiable`, by móc zostać poprawnie użyta do rysowania listy dostępnych klientów w pobliżu użytkownika.
|
||
|
||
```swift
|
||
struct Peer: Identifiable {
|
||
enum ConnectionState {
|
||
case available
|
||
case joined
|
||
case rejected
|
||
case invitationPending
|
||
}
|
||
|
||
var id: String { mcPeer.displayName }
|
||
let mcPeer: MCPeerID
|
||
var state: ConnectionState
|
||
}
|
||
```
|
||
|
||
== Warstwa sieciowa i komunikacja P2P
|
||
Całość komunikacji między urządzeniami odbywa się z wykorzystaniem frameworka Multipeer Connectivity. Klient twórcy notatki pełni rolę serwera, a pozostali użytkownicy, po uprzednim zaproszeniu, mogą dołączyć do edycji notatki, wysyłać swoje zmiany jak i odbierać zmiany, które dystrybuuje serwer.
|
||
|
||
== Odkrywanie innych urządzeń
|
||
Obiekt reprezentujący serwer został nazwany `NoteEditingSessionServer`, który dziedziczy właściowści po klasie `NSObject`, która jest uniwersalną implementacją wielu zachowań, które są wymagane od frameworków udostępnianych przez Apple, które zostały napisane w języku Objective-C. Jego konstruktor w przyjmowanych argumentach oczekuje tylko obiektu `OwnPeer`, który będzie wykorzystywany do identyfikacji instancji aplikacji u innych klientów. Sama implementacja konstruktora tworzy nową sesję `MCSession`; obiekt `MCNearbyServiceBrowser`, który odpowiada za wykrywanie pobliskich klientów. Finalnie przypisuje referencję do samego siebie jako parametr `delegate` dla utworzonych `MCSession` i `MCNearbyServiceBrowser`. Pozwala nam to zaimplementować metody, które będą wykorzystywane wewnątrz tych obiektów do komunikacji z innymi użytkownikami. Protokoły delegujące dla wspomnianych obiektów nazywają się odpowiednio `MCSessionDelegate` oraz `MCNearbyServiceBrowserDelegate`. Moja implementacja tych protokołów zostanie przedstawiona w dalszej części pracy.
|
||
|
||
```swift
|
||
init(peer: OwnPeer) {
|
||
ownPeer = peer
|
||
browser = .init(peer: peer.peer, serviceType: "peered")
|
||
session = .init(peer: peer.peer, securityIdentity: nil, encryptionPreference: .required)
|
||
super.init() // wykonuje pozostałą część
|
||
browser.delegate = self //
|
||
session.delegate = self
|
||
}
|
||
```
|
||
|
||
W momencie, gdy autor notatki otworzy ekran edycji, wykonuje się metoda `startServer()`, która wywołuje metodę `startBrowsingForPeers()` obiektu `MCNearbyServiceBrowser`. Opuszczenie ekranu edycji wywołuje metodę `stopServer()`, która wywołuje analogiczną metodę `stopBrowsingForPeers()` oraz zatrzymuje sesję poprzez wywołanie metody `disconnect()` obiektu `MCSession`.
|
||
|
||
```swift
|
||
func startServer() {
|
||
browser.startBrowsingForPeers()
|
||
}
|
||
|
||
func stopServer() {
|
||
browser.stopBrowsingForPeers()
|
||
session.disconnect()
|
||
}
|
||
```
|
||
|
||
Obiekt `browser` w momencie wykrycia nowego użytkownika w pobliżu, wywołuje naszą metodę o nazwie `browser`, która przyjmuje wszystkie potrzebne informacje o znalezionym użytkowniku. Implementacja mojego systemu następnie upewnia się czy odkryty użytkownik nie jest jednocześnie autorem notatki, co jest znanym błędem w Multipeer Connectivity, a następnie po udanej weryfikacji dodajemy nowy obiekt dostępnego użytkownika do tablicy na podstawie której jest budowany interfejs z listą dostępnych użytkowników.
|
||
|
||
== Transportowanie danych
|
||
== Algorytm rozwiązywania konfliktów
|
||
== Środowisko developerskie i stack technologiczny
|
||
== Implementacja logiki P2P
|
||
== Interfejs użytkownika
|
||
== Napotkane wyzwania implementacyjne i rozwiązania
|
||
== Ograniczenia środowisk iOS/macOS
|