Use Dependency propertyWrapper

This commit is contained in:
Djorkaeff Alexandre 2024-01-19 18:04:47 -03:00
parent cf1cbb7d61
commit 91968be958
16 changed files with 155 additions and 209 deletions

View File

@ -0,0 +1,40 @@
import SwiftUI
struct AppView: View {
@Dependency private var database: Database
@Dependency private var serversDB: ServersDatabase
@Storage(.currentServer) private var currentURL: URL?
@StateObject private var router: AppRouter
init(router: AppRouter) {
_router = StateObject(wrappedValue: router)
}
var body: some View {
NavigationStack {
switch router.route {
case .roomList(let server):
RoomListView(server: server)
.environment(\.managedObjectContext, database.viewContext)
case .serverList:
ServerListView(viewModel: ServerListViewModel())
.environment(\.managedObjectContext, serversDB.viewContext)
}
}
.onAppear {
loadRoute()
}
}
private func loadRoute() {
if let currentURL, let server = serversDB.server(url: currentURL) {
router.route(to: .roomList(server))
} else if serversDB.servers().count == 1, let server = serversDB.servers().first {
router.route(to: .roomList(server))
} else {
router.route(to: .serverList)
}
}
}

View File

@ -12,7 +12,11 @@ protocol RocketChatClientProtocol {
}
final class RocketChatClient: NSObject {
private let server: Server
@Dependency private var serverProvider: ServerProviding
private var server: Server {
serverProvider.current()
}
private lazy var session = URLSession(
configuration: .default,
@ -23,10 +27,6 @@ final class RocketChatClient: NSObject {
delegateQueue: nil
)
init(server: Server) {
self.server = server
}
private var adapters: [RequestAdapter] {
[
TokenAdapter(server: server),

View File

@ -13,7 +13,7 @@ protocol Database {
}
final class RocketChatDatabase: Database {
private let container: NSPersistentContainer
@Dependency private var serverProvider: ServerProviding
var viewContext: NSManagedObjectContext {
container.viewContext
@ -28,15 +28,19 @@ final class RocketChatDatabase: Database {
return managedObjectModel
}()
init(name: String) {
container = NSPersistentContainer(name: name, managedObjectModel: Self.model)
private lazy var container: NSPersistentContainer = {
let name = serverProvider.current().url.host ?? "default"
let container = NSPersistentContainer(name: name, managedObjectModel: Self.model)
container.loadPersistentStores { _, error in
if let error { fatalError("Can't load persistent stores: \(error)") }
}
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
return container
}()
private func save() {
guard container.viewContext.hasChanges else {

View File

@ -1,10 +1,21 @@
@propertyWrapper
struct Dependency<T> {
var wrappedValue: T {
private var dependency: T
init() {
guard let dependency = Store.resolve(T.self) else {
fatalError("No service of type \(ObjectIdentifier(T.self)) registered!")
fatalError("No service of type \(T.self) registered!")
}
return dependency
self.dependency = dependency
}
var wrappedValue: T {
get {
dependency
}
mutating set {
dependency = newValue
}
}
}

View File

@ -1,29 +0,0 @@
final class DependencyStore {
func client(for server: Server) -> RocketChatClientProtocol {
RocketChatClient(server: server)
}
let connection = WatchConnection(session: .default)
let database = DefaultDatabase()
private var activeDatabase: WeakRef<RocketChatDatabase>?
func database(for server: Server) -> RocketChatDatabase {
if let activeDatabase = activeDatabase?.value {
return activeDatabase
}
let database = RocketChatDatabase(name: server.url.host ?? "server")
activeDatabase = WeakRef(value: database)
return database
}
}
private final class WeakRef<T: AnyObject> {
weak var value: T?
init(value: T) {
self.value = value
}
}

View File

@ -7,7 +7,7 @@ protocol StoreInterface {
final class Store: StoreInterface {
private static var factories: [ObjectIdentifier: () -> Any] = [:]
private static var cache: [ObjectIdentifier: Any] = [:]
private static var cache: [ObjectIdentifier: WeakRef<AnyObject>] = [:]
static func register<T>(_ type: T.Type, factory: @autoclosure @escaping () -> T) {
let identifier = ObjectIdentifier(type)
@ -17,13 +17,13 @@ final class Store: StoreInterface {
static func resolve<T>(_ type: T.Type) -> T? {
let identifier = ObjectIdentifier(type)
if let dependency = cache[identifier] {
if let dependency = cache[identifier]?.value {
return dependency as? T
} else {
let dependency = factories[identifier]?() as? T
if let dependency {
cache[identifier] = dependency
cache[identifier] = WeakRef(value: dependency as AnyObject)
}
return dependency
@ -32,7 +32,7 @@ final class Store: StoreInterface {
}
private final class WeakRef<T: AnyObject> {
weak var value: T?
private(set) weak var value: T?
init(value: T) {
self.value = value

View File

@ -6,26 +6,16 @@ protocol MessageSending {
}
final class MessageSender {
private let client: RocketChatClientProtocol
private let database: Database
private let server: Server
init(
client: RocketChatClientProtocol,
database: Database,
server: Server
) {
self.client = client
self.database = database
self.server = server
}
@Dependency private var client: RocketChatClientProtocol
@Dependency private var database: Database
@Dependency private var serverProvider: ServerProviding
}
extension MessageSender: MessageSending {
func sendMessage(_ msg: String, in room: Room) {
guard let rid = room.id else { return }
let messageID = database.createTempMessage(msg: msg, in: room, for: server.loggedUser)
let messageID = database.createTempMessage(msg: msg, in: room, for: serverProvider.current().loggedUser)
client.sendMessage(id: messageID, rid: rid, msg: msg)
.receive(on: DispatchQueue.main)

View File

@ -13,22 +13,12 @@ final class MessagesLoader {
private var timer: Timer?
private var cancellable = CancelBag()
private let client: RocketChatClientProtocol
private let database: Database
private let serversDB: ServersDatabase
@Dependency private var client: RocketChatClientProtocol
@Dependency private var database: Database
@Dependency private var serversDB: ServersDatabase
private var roomID: String?
init(
client: RocketChatClientProtocol,
database: Database,
serversDB: ServersDatabase
) {
self.client = client
self.database = database
self.serversDB = serversDB
}
private func scheduledSyncMessages(in room: Room, from date: Date) {
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in
self?.syncMessages(in: room, from: date)

View File

@ -8,19 +8,13 @@ protocol RoomsLoading {
}
final class RoomsLoader {
@Dependency private var client: RocketChatClientProtocol
@Dependency private var database: Database
@Dependency private var serversDB: ServersDatabase
private var timer: Timer?
private var cancellable = CancelBag()
private let client: RocketChatClientProtocol
private let database: Database
private let serversDB: ServersDatabase
init(client: RocketChatClientProtocol, database: Database, serversDB: ServersDatabase) {
self.client = client
self.database = database
self.serversDB = serversDB
}
private func scheduledLoadRooms(in server: Server) {
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { [weak self] _ in
self?.loadRooms(in: server)

View File

@ -2,65 +2,27 @@ import SwiftUI
@main
struct RocketChat_Watch_AppApp: App {
private let store: DependencyStore
@StateObject var router: RocketChatAppRouter
private let router = AppRouter()
init() {
let store = DependencyStore()
self.store = store
self._router = StateObject(wrappedValue: RocketChatAppRouter(database: store.database))
registerDependencies()
}
@ViewBuilder
private var serverListView: some View {
ServerListView(
dependencies: .init(
connection: store.connection,
database: store.database,
router: router
)
)
}
@ViewBuilder
private func roomListView(for server: Server) -> some View {
RoomListView(
client: store.client(for: server),
database: store.database(for: server),
messagesLoader: MessagesLoader(
client: store.client(for: server),
database: store.database(for: server),
serversDB: store.database
),
messageSender: MessageSender(
client: store.client(for: server),
database: store.database(for: server),
server: server
),
roomsLoader: RoomsLoader(
client: store.client(for: server),
database: store.database(for: server),
serversDB: store.database
),
router: router,
server: server
)
private func registerDependencies() {
Store.register(ServersDatabase.self, factory: DefaultDatabase())
Store.register(ServerProviding.self, factory: ServerProvider())
Store.register(Connection.self, factory: WatchConnection(session: .default))
Store.register(RocketChatClientProtocol.self, factory: RocketChatClient())
Store.register(Database.self, factory: RocketChatDatabase())
Store.register(MessagesLoading.self, factory: MessagesLoader())
Store.register(MessageSending.self, factory: MessageSender())
Store.register(RoomsLoading.self, factory: RoomsLoader())
Store.register(AppRouting.self, factory: router)
}
var body: some Scene {
WindowGroup {
NavigationStack {
switch router.route {
case .roomList(let server):
roomListView(for: server)
.environment(\.managedObjectContext, store.database(for: server).viewContext)
case .serverList:
serverListView
.environment(\.managedObjectContext, store.database.viewContext)
}
}
AppView(router: router)
}
}
}

View File

@ -1,48 +1,29 @@
import Foundation
protocol RocketChatAppRouting {
protocol AppRouting {
func route(to route: Route)
}
final class RocketChatAppRouter: ObservableObject, RocketChatAppRouting {
@Storage(.currentServer) private var currentServer: URL?
final class AppRouter: ObservableObject {
@Storage(.currentServer) private var currentURL: URL?
@Published var route: Route = .serverList {
didSet {
switch route {
case .roomList(let server):
currentServer = server.url
case .serverList:
break
}
}
}
private let database: ServersDatabase
init(database: ServersDatabase) {
self.database = database
loadRoute()
}
private func loadRoute() {
if let currentServer, let server = database.server(url: currentServer) {
route = .roomList(server)
} else if database.servers().count == 1, let server = database.servers().first {
route = .roomList(server)
} else {
route = .serverList
}
}
@Published private(set) var route: Route = .serverList
}
extension AppRouter: AppRouting {
func route(to route: Route) {
DispatchQueue.main.async {
self.route = route
switch route {
case .roomList(let server):
currentURL = server.url
case .serverList:
break
}
self.route = route
}
}
enum Route {
case roomList(Server)
case serverList
case roomList(Server)
}

View File

@ -0,0 +1,20 @@
import Foundation
protocol ServerProviding {
func current() -> Server
}
final class ServerProvider {
@Storage(.currentServer) private var currentURL: URL?
@Dependency private var database: ServersDatabase
}
extension ServerProvider: ServerProviding {
func current() -> Server {
if let currentURL, let server = database.server(url: currentURL) {
return server
} else {
fatalError("Attempt to get server before it was not set.")
}
}
}

View File

@ -7,22 +7,14 @@ enum ServerListState {
}
final class ServerListViewModel: ObservableObject {
struct Dependencies {
let connection: Connection
let database: ServersDatabase
let router: RocketChatAppRouter
}
private let dependencies: Dependencies
@Dependency private var connection: Connection
@Dependency private var database: ServersDatabase
@Dependency private var router: AppRouting
@Published private(set) var state: ServerListState = .loading
init(dependencies: Dependencies) {
self.dependencies = dependencies
}
private func handleSuccess(message: WatchMessage) {
message.servers.forEach(dependencies.database.process(updatedServer:))
message.servers.forEach(database.process(updatedServer:))
state = .loaded
}
@ -35,7 +27,7 @@ final class ServerListViewModel: ObservableObject {
}
func loadServers() {
dependencies.connection.sendMessage { [weak self] result in
connection.sendMessage { [weak self] result in
guard let self else {
return
}
@ -50,6 +42,6 @@ final class ServerListViewModel: ObservableObject {
}
func didTap(server: Server) {
dependencies.router.route(to: .roomList(server))
router.route(to: .roomList(server))
}
}

View File

@ -1,31 +1,18 @@
import SwiftUI
struct RoomListView: View {
private let client: RocketChatClientProtocol
private let database: Database
private let messagesLoader: MessagesLoading
private let messageSender: MessageSending
private let roomsLoader: RoomsLoading
private let router: RocketChatAppRouter
@Dependency private var client: RocketChatClientProtocol
@Dependency private var database: Database
@Dependency private var messagesLoader: MessagesLoading
@Dependency private var messageSender: MessageSending
@Dependency private var roomsLoader: RoomsLoading
@Dependency private var router: AppRouting
private let server: Server
@FetchRequest<Room> private var rooms: FetchedResults<Room>
init(
client: RocketChatClientProtocol,
database: Database,
messagesLoader: MessagesLoading,
messageSender: MessageSending,
roomsLoader: RoomsLoading,
router: RocketChatAppRouter,
server: Server
) {
self.client = client
self.database = database
self.messagesLoader = messagesLoader
self.messageSender = messageSender
self.roomsLoader = roomsLoader
self.router = router
init(server: Server) {
self.server = server
_rooms = FetchRequest(fetchRequest: server.roomsRequest)
}

View File

@ -6,8 +6,8 @@ struct ServerListView: View {
@FetchRequest(entity: Server.entity(), sortDescriptors: [], animation: .default)
private var servers: FetchedResults<Server>
init(dependencies: ServerListViewModel.Dependencies) {
_viewModel = StateObject(wrappedValue: ServerListViewModel(dependencies: dependencies))
init(viewModel: ServerListViewModel) {
_viewModel = StateObject(wrappedValue: viewModel)
}
@ViewBuilder

View File

@ -80,6 +80,8 @@
1E4AFC152B5AF09800E2AA7D /* Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4AFC142B5AF09800E2AA7D /* Dependency.swift */; };
1E4AFC172B5AF09C00E2AA7D /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4AFC162B5AF09C00E2AA7D /* Store.swift */; };
1E4AFC1B2B5AFC6A00E2AA7D /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4AFC1A2B5AFC6A00E2AA7D /* Publisher+Extensions.swift */; };
1E4AFC1F2B5B0D0500E2AA7D /* ServerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4AFC1E2B5B0D0500E2AA7D /* ServerProvider.swift */; };
1E4AFC212B5B1AA000E2AA7D /* AppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4AFC202B5B1AA000E2AA7D /* AppView.swift */; };
1E51D962251263CD00DC95DE /* MessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E51D961251263CD00DC95DE /* MessageType.swift */; };
1E51D965251263D600DC95DE /* NotificationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E51D964251263D600DC95DE /* NotificationType.swift */; };
1E598AE42515057D002BDFBD /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E598AE32515057D002BDFBD /* Date+Extensions.swift */; };
@ -130,7 +132,6 @@
1ED033BF2B55BF94004F4930 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033BE2B55BF94004F4930 /* Storage.swift */; };
1ED033C12B55C190004F4930 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1ED033C02B55C190004F4930 /* Localizable.xcstrings */; };
1ED033C42B55C65C004F4930 /* RocketChatAppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033C32B55C65C004F4930 /* RocketChatAppRouter.swift */; };
1ED033C82B55CE78004F4930 /* DependencyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033C72B55CE78004F4930 /* DependencyStore.swift */; };
1ED033CB2B55D4F0004F4930 /* RocketChat.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033C92B55D4F0004F4930 /* RocketChat.xcdatamodeld */; };
1ED033CD2B55D671004F4930 /* RocketChatDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED033CC2B55D671004F4930 /* RocketChatDatabase.swift */; };
1ED038912B507B4C00C007D4 /* RocketChatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED038902B507B4C00C007D4 /* RocketChatApp.swift */; };
@ -391,6 +392,8 @@
1E4AFC142B5AF09800E2AA7D /* Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependency.swift; sourceTree = "<group>"; };
1E4AFC162B5AF09C00E2AA7D /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
1E4AFC1A2B5AFC6A00E2AA7D /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = "<group>"; };
1E4AFC1E2B5B0D0500E2AA7D /* ServerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerProvider.swift; sourceTree = "<group>"; };
1E4AFC202B5B1AA000E2AA7D /* AppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppView.swift; sourceTree = "<group>"; };
1E51D961251263CD00DC95DE /* MessageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageType.swift; sourceTree = "<group>"; };
1E51D964251263D600DC95DE /* NotificationType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationType.swift; sourceTree = "<group>"; };
1E598AE32515057D002BDFBD /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
@ -420,7 +423,6 @@
1ED033BE2B55BF94004F4930 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
1ED033C02B55C190004F4930 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
1ED033C32B55C65C004F4930 /* RocketChatAppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RocketChatAppRouter.swift; sourceTree = "<group>"; };
1ED033C72B55CE78004F4930 /* DependencyStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyStore.swift; sourceTree = "<group>"; };
1ED033CA2B55D4F0004F4930 /* RocketChat.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = RocketChat.xcdatamodel; sourceTree = "<group>"; };
1ED033CC2B55D671004F4930 /* RocketChatDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RocketChatDatabase.swift; sourceTree = "<group>"; };
1ED0388E2B507B4B00C007D4 /* Rocket.Chat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Rocket.Chat.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -815,11 +817,13 @@
1ED033AB2B55B1C2004F4930 /* Database */,
1ED038902B507B4C00C007D4 /* RocketChatApp.swift */,
1ED033C32B55C65C004F4930 /* RocketChatAppRouter.swift */,
1E4AFC202B5B1AA000E2AA7D /* AppView.swift */,
1ED038C92B50A58400C007D4 /* WatchConnection.swift */,
1ED033BE2B55BF94004F4930 /* Storage.swift */,
1ED038942B507B4D00C007D4 /* Assets.xcassets */,
1ED038962B507B4D00C007D4 /* Preview Content */,
1ED033C02B55C190004F4930 /* Localizable.xcstrings */,
1E4AFC1E2B5B0D0500E2AA7D /* ServerProvider.swift */,
);
path = "RocketChat Watch App";
sourceTree = "<group>";
@ -865,7 +869,6 @@
1EDFD0FB2B589FC4002FEE5F /* DependencyInjection */ = {
isa = PBXGroup;
children = (
1ED033C72B55CE78004F4930 /* DependencyStore.swift */,
1E4AFC142B5AF09800E2AA7D /* Dependency.swift */,
1E4AFC162B5AF09C00E2AA7D /* Store.swift */,
);
@ -1882,6 +1885,7 @@
1E29A3052B585B070093C03C /* Request.swift in Sources */,
1E9A71772B59FCA900477BA2 /* URLSessionCertificateHandling.swift in Sources */,
1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */,
1E4AFC212B5B1AA000E2AA7D /* AppView.swift in Sources */,
1E29A2FB2B585B070093C03C /* MessagesRequest.swift in Sources */,
1E29A31D2B5871B60093C03C /* Date+Extensions.swift in Sources */,
1E29A2F62B585B070093C03C /* UserResponse.swift in Sources */,
@ -1889,6 +1893,7 @@
1ED033BF2B55BF94004F4930 /* Storage.swift in Sources */,
1E29A2F82B585B070093C03C /* MessageResponse.swift in Sources */,
1E29A3042B585B070093C03C /* HTTPMethod.swift in Sources */,
1E4AFC1F2B5B0D0500E2AA7D /* ServerProvider.swift in Sources */,
1E29A3012B585B070093C03C /* RequestAdapter.swift in Sources */,
1E29A2F52B585B070093C03C /* RoomsResponse.swift in Sources */,
1E29A2F32B585B070093C03C /* MessagesResponse.swift in Sources */,
@ -1902,7 +1907,6 @@
1ED033C42B55C65C004F4930 /* RocketChatAppRouter.swift in Sources */,
1ED033B02B55B25A004F4930 /* Database.swift in Sources */,
1E9A71712B59CC1300477BA2 /* Attachment.swift in Sources */,
1ED033C82B55CE78004F4930 /* DependencyStore.swift in Sources */,
1E29A30A2B585B370093C03C /* Data+Extensions.swift in Sources */,
1E29A2F72B585B070093C03C /* ReadResponse.swift in Sources */,
1ED033CB2B55D4F0004F4930 /* RocketChat.xcdatamodeld in Sources */,