From 038385c2c724255759203a4a8a9e07e231128d56 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Tue, 1 Sep 2020 00:33:49 -0700 Subject: [PATCH] Modularize view models --- Extensions/String+Extensions.swift | 16 -- Extensions/View+Extensions.swift | 1 + HTTP/Sources/HTTP/Client.swift | 2 +- .../Stubbing/StubbingURLProtocol.swift | 3 +- .../MastodonStubs/Paged+Stubbing.swift | 11 ++ Metatext.xcodeproj/project.pbxproj | 155 ++---------------- .../xcshareddata/swiftpm/Package.resolved | 2 +- Model/AlertItem.swift | 8 - Preview/PreviewMocks.swift | 124 -------------- .../ServiceLayer/AllIdentitiesService.swift | 2 +- .../Database/ContentDatabase.swift | 4 +- .../Database/IdentityDatabase.swift | 26 ++- .../Entities/AppEnvironment.swift | 39 ++++- .../Entities/UserNotificationClient.swift | 68 ++++++++ .../Entities/WebAuthSession.swift | 5 +- .../ServiceLayer/IdentityService.swift | 2 +- .../UserNotificationService.swift | 38 ++--- .../MockAppEnvironment.swift | 18 +- .../MockUserNotificationClient.swift | 11 ++ .../AuthenticationServiceTests.swift | 2 +- System/MetatextApp.swift | 12 +- .../StatusListViewController.swift | 1 + ViewModels/.gitignore | 5 + ViewModels/Package.swift | 34 ++++ .../PreviewViewModels/ViewModelMocks.swift | 103 ++++++++++++ .../ViewModels}/AddIdentityViewModel.swift | 14 +- .../ViewModels}/AttachmentViewModel.swift | 6 +- .../ViewModels}/EditFilterViewModel.swift | 14 +- .../ViewModels/Entities/AlertItem.swift | 8 + .../Extensions}/Date+Extensions.swift | 0 .../Extensions}/Publisher+Extensions.swift | 0 .../Extensions/String+Extensions.swift | 21 +++ .../ViewModels}/FiltersViewModel.swift | 10 +- .../ViewModels}/IdentitiesViewModel.swift | 8 +- .../Sources/ViewModels}/ListsViewModel.swift | 10 +- ...otificationTypesPreferencesViewModel.swift | 6 +- .../PostingReadingPreferencesViewModel.swift | 6 +- .../ViewModels}/PreferencesViewModel.swift | 8 +- .../Sources/ViewModels}/RootViewModel.swift | 27 ++- .../SecondaryNavigationViewModel.swift | 7 +- .../ViewModels}/StatusListViewModel.swift | 14 +- .../Sources/ViewModels}/StatusViewModel.swift | 38 ++--- .../ViewModels}/TabNavigationViewModel.swift | 42 ++--- .../AddIdentityViewModelTests.swift | 18 +- .../ViewModelsTests}/RootViewModelTests.swift | 9 +- Views/AddIdentityView.swift | 5 +- Views/Attachments/AttachmentView.swift | 1 + Views/Attachments/AttachmentsView.swift | 1 + Views/EditFilterView.swift | 5 +- Views/FiltersView.swift | 5 +- Views/IdentitiesView.swift | 8 +- Views/ListsView.swift | 7 +- Views/NotificationTypesPreferencesView.swift | 5 +- Views/PostingReadingPreferencesView.swift | 5 +- Views/PreferencesView.swift | 5 +- Views/RootView.swift | 5 +- Views/SecondaryNavigationView.swift | 9 +- Views/Status Cell/StatusTableViewCell.swift | 1 + Views/StatusListView.swift | 5 +- Views/TabNavigationView.swift | 27 ++- 60 files changed, 561 insertions(+), 491 deletions(-) create mode 100644 Mastodon/Sources/MastodonStubs/Paged+Stubbing.swift delete mode 100644 Model/AlertItem.swift delete mode 100644 Preview/PreviewMocks.swift create mode 100644 ServiceLayer/Sources/ServiceLayer/Entities/UserNotificationClient.swift create mode 100644 ServiceLayer/Sources/ServiceLayerMocks/MockUserNotificationClient.swift create mode 100644 ViewModels/.gitignore create mode 100644 ViewModels/Package.swift create mode 100644 ViewModels/Sources/PreviewViewModels/ViewModelMocks.swift rename {View Models => ViewModels/Sources/ViewModels}/AddIdentityViewModel.swift (86%) rename {View Models => ViewModels/Sources/ViewModels}/AttachmentViewModel.swift (82%) rename {View Models => ViewModels/Sources/ViewModels}/EditFilterViewModel.swift (84%) create mode 100644 ViewModels/Sources/ViewModels/Entities/AlertItem.swift rename {Extensions => ViewModels/Sources/ViewModels/Extensions}/Date+Extensions.swift (100%) rename {Extensions => ViewModels/Sources/ViewModels/Extensions}/Publisher+Extensions.swift (100%) create mode 100644 ViewModels/Sources/ViewModels/Extensions/String+Extensions.swift rename {View Models => ViewModels/Sources/ViewModels}/FiltersViewModel.swift (84%) rename {View Models => ViewModels/Sources/ViewModels}/IdentitiesViewModel.swift (70%) rename {View Models => ViewModels/Sources/ViewModels}/ListsViewModel.swift (85%) rename {View Models => ViewModels/Sources/ViewModels}/NotificationTypesPreferencesViewModel.swift (87%) rename {View Models => ViewModels/Sources/ViewModels}/PostingReadingPreferencesViewModel.swift (81%) rename {View Models => ViewModels/Sources/ViewModels}/PreferencesViewModel.swift (82%) rename {View Models => ViewModels/Sources/ViewModels}/RootViewModel.swift (75%) rename {View Models => ViewModels/Sources/ViewModels}/SecondaryNavigationViewModel.swift (81%) rename {View Models => ViewModels/Sources/ViewModels}/StatusListViewModel.swift (90%) rename {View Models => ViewModels/Sources/ViewModels}/StatusViewModel.swift (82%) rename {View Models => ViewModels/Sources/ViewModels}/TabNavigationViewModel.swift (73%) rename {Tests/View Models => ViewModels/Tests/ViewModelsTests}/AddIdentityViewModelTests.swift (78%) rename {Tests/View Models => ViewModels/Tests/ViewModelsTests}/RootViewModelTests.swift (78%) diff --git a/Extensions/String+Extensions.swift b/Extensions/String+Extensions.swift index c50e328..1be4358 100644 --- a/Extensions/String+Extensions.swift +++ b/Extensions/String+Extensions.swift @@ -3,22 +3,6 @@ import UIKit extension String { - private static let HTTPSPrefix = "https://" - - func url() throws -> URL { - let url: URL? - - if hasPrefix(Self.HTTPSPrefix) { - url = URL(string: self) - } else { - url = URL(string: Self.HTTPSPrefix + self) - } - - guard let validURL = url else { throw URLError(.badURL) } - - return validURL - } - func countEmphasizedAttributedString(count: Int, highlighted: Bool = false) -> NSAttributedString { let countRange = (self as NSString).range(of: String.localizedStringWithFormat("%ld", count)) diff --git a/Extensions/View+Extensions.swift b/Extensions/View+Extensions.swift index 29cf5d3..2fe3524 100644 --- a/Extensions/View+Extensions.swift +++ b/Extensions/View+Extensions.swift @@ -2,6 +2,7 @@ import Foundation import SwiftUI +import ViewModels extension View { func alertItem(_ alertItem: Binding) -> some View { diff --git a/HTTP/Sources/HTTP/Client.swift b/HTTP/Sources/HTTP/Client.swift index fb96b1b..df60f40 100644 --- a/HTTP/Sources/HTTP/Client.swift +++ b/HTTP/Sources/HTTP/Client.swift @@ -52,6 +52,6 @@ private extension Client { return session.request(target) .validate() - .publishDecodable(type: T.ResultType.self, decoder: decoder) + .publishDecodable(type: T.ResultType.self, queue: session.rootQueue, decoder: decoder) } } diff --git a/HTTP/Sources/Stubbing/StubbingURLProtocol.swift b/HTTP/Sources/Stubbing/StubbingURLProtocol.swift index 337ff48..fa977f5 100644 --- a/HTTP/Sources/Stubbing/StubbingURLProtocol.swift +++ b/HTTP/Sources/Stubbing/StubbingURLProtocol.swift @@ -22,7 +22,8 @@ public class StubbingURLProtocol: URLProtocol { guard let url = request.url, let stub = Self.stub(request: request, target: Self.targetsForURLs[url]) else { - preconditionFailure("Stub for request not found") +// preconditionFailure("Stub for request not found") + return } switch stub { diff --git a/Mastodon/Sources/MastodonStubs/Paged+Stubbing.swift b/Mastodon/Sources/MastodonStubs/Paged+Stubbing.swift new file mode 100644 index 0000000..dd390d9 --- /dev/null +++ b/Mastodon/Sources/MastodonStubs/Paged+Stubbing.swift @@ -0,0 +1,11 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import Mastodon +import Stubbing + +extension Paged: Stubbing where T: Stubbing { + public func data(url: URL) -> Data? { + endpoint.data(url: url) + } +} diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 18a9bd5..e662c19 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -7,25 +7,18 @@ objects = { /* Begin PBXBuildFile section */ + D0175CAC24FE2D6300B085F6 /* PreviewViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0175CAB24FE2D6300B085F6 /* PreviewViewModels */; }; D01F41D724F880C400D55A2D /* StatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D01F41D424F880C400D55A2D /* StatusTableViewCell.xib */; }; D01F41D824F880C400D55A2D /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D524F880C400D55A2D /* StatusTableViewCell.swift */; }; D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; }; - D01F41DF24F8868800D55A2D /* AttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41DE24F8868800D55A2D /* AttachmentViewModel.swift */; }; D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* AttachmentsView.swift */; }; - D04FD74224D4AA34007D572D /* PreviewMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04FD74124D4AA34007D572D /* PreviewMocks.swift */; }; - D052BBC724D749C800A80A7A /* RootViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D052BBC624D749C800A80A7A /* RootViewModelTests.swift */; }; - D065F53924D37E5100741304 /* CombineExpectations in Frameworks */ = {isa = PBXBuildFile; productRef = D065F53824D37E5100741304 /* CombineExpectations */; }; D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = D06B492224D4611300642749 /* KingfisherSwiftUI */; }; - D0BDF66724FD7CDA00C7FA1C /* ServiceLayer in Frameworks */ = {isa = PBXBuildFile; productRef = D0BDF66624FD7CDA00C7FA1C /* ServiceLayer */; }; D0BDF66B24FD7CEC00C7FA1C /* ServiceLayer in Frameworks */ = {isa = PBXBuildFile; productRef = D0BDF66A24FD7CEC00C7FA1C /* ServiceLayer */; }; D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */; }; D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */; }; - D0BEB1FD24F9E4E5001B0F04 /* ListsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */; }; D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */; }; D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20424FA1107001B0F04 /* FiltersView.swift */; }; - D0BEB20724FA1121001B0F04 /* FiltersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB20624FA1121001B0F04 /* FiltersViewModel.swift */; }; D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */; }; - D0BEB21324FA2C0A001B0F04 /* EditFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB21224FA2C0A001B0F04 /* EditFilterViewModel.swift */; }; D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42224F76169001EBDBB /* IdentitiesView.swift */; }; D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */; }; D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42424F76169001EBDBB /* AddIdentityView.swift */; }; @@ -37,34 +30,20 @@ D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */; }; D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */; }; D0C7D4A524F7616A001EBDBB /* StatusListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D43124F76169001EBDBB /* StatusListViewController.swift */; }; - D0C7D4C024F7616A001EBDBB /* AlertItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45024F76169001EBDBB /* AlertItem.swift */; }; D0C7D4C224F7616A001EBDBB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0C7D45224F76169001EBDBB /* Assets.xcassets */; }; D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45424F76169001EBDBB /* MetatextApp.swift */; }; D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45524F76169001EBDBB /* AppDelegate.swift */; }; D0C7D4C524F7616A001EBDBB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D0C7D45724F76169001EBDBB /* Localizable.strings */; }; D0C7D4C624F7616A001EBDBB /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D0C7D45824F76169001EBDBB /* Localizable.stringsdict */; }; - D0C7D4C724F7616A001EBDBB /* PostingReadingPreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45A24F76169001EBDBB /* PostingReadingPreferencesViewModel.swift */; }; - D0C7D4C824F7616A001EBDBB /* SecondaryNavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45B24F76169001EBDBB /* SecondaryNavigationViewModel.swift */; }; - D0C7D4C924F7616A001EBDBB /* TabNavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45C24F76169001EBDBB /* TabNavigationViewModel.swift */; }; - D0C7D4CA24F7616A001EBDBB /* NotificationTypesPreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45D24F76169001EBDBB /* NotificationTypesPreferencesViewModel.swift */; }; - D0C7D4CB24F7616A001EBDBB /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45E24F76169001EBDBB /* RootViewModel.swift */; }; - D0C7D4CC24F7616A001EBDBB /* IdentitiesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45F24F76169001EBDBB /* IdentitiesViewModel.swift */; }; - D0C7D4CD24F7616A001EBDBB /* AddIdentityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46024F76169001EBDBB /* AddIdentityViewModel.swift */; }; - D0C7D4CE24F7616A001EBDBB /* PreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46124F76169001EBDBB /* PreferencesViewModel.swift */; }; - D0C7D4CF24F7616A001EBDBB /* StatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46224F76169001EBDBB /* StatusViewModel.swift */; }; - D0C7D4D024F7616A001EBDBB /* StatusListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46324F76169001EBDBB /* StatusListViewModel.swift */; }; D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46A24F76169001EBDBB /* String+Extensions.swift */; }; D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */; }; D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */; }; - D0C7D4D824F7616A001EBDBB /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46D24F76169001EBDBB /* Publisher+Extensions.swift */; }; D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; }; D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46F24F76169001EBDBB /* View+Extensions.swift */; }; - D0C7D4DB24F7616A001EBDBB /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D47024F76169001EBDBB /* Date+Extensions.swift */; }; D0C7D4DC24F7616A001EBDBB /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D47124F76169001EBDBB /* Data+Extensions.swift */; }; - D0E2C1CE24FD7EE900854680 /* ServiceLayerMocks in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1CD24FD7EE900854680 /* ServiceLayerMocks */; }; + D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; }; D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; }; D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -102,22 +81,16 @@ D01F41D424F880C400D55A2D /* StatusTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = StatusTableViewCell.xib; sourceTree = ""; }; D01F41D524F880C400D55A2D /* StatusTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = ""; }; - D01F41DE24F8868800D55A2D /* AttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentViewModel.swift; sourceTree = ""; }; D01F41E224F8889700D55A2D /* AttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentsView.swift; sourceTree = ""; }; D047FA8C24C3E21200AF17C5 /* Metatext.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Metatext.app; sourceTree = BUILT_PRODUCTS_DIR; }; - D04FD74124D4AA34007D572D /* PreviewMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewMocks.swift; sourceTree = ""; }; - D052BBC624D749C800A80A7A /* RootViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModelTests.swift; sourceTree = ""; }; D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D0BDF66524FD7A6400C7FA1C /* ServiceLayer */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ServiceLayer; sourceTree = ""; }; D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = ""; }; D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableFooterView.swift; sourceTree = ""; }; - D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsViewModel.swift; sourceTree = ""; }; D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsView.swift; sourceTree = ""; }; D0BEB20424FA1107001B0F04 /* FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersView.swift; sourceTree = ""; }; - D0BEB20624FA1121001B0F04 /* FiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewModel.swift; sourceTree = ""; }; D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = ""; }; - D0BEB21224FA2C0A001B0F04 /* EditFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterViewModel.swift; sourceTree = ""; }; D0BFDAF524FC7C5300C86618 /* HTTP */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HTTP; sourceTree = ""; }; D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = ""; }; D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -132,36 +105,23 @@ D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationTypesPreferencesView.swift; sourceTree = ""; }; D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabNavigationView.swift; sourceTree = ""; }; D0C7D43124F76169001EBDBB /* StatusListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusListViewController.swift; sourceTree = ""; }; - D0C7D45024F76169001EBDBB /* AlertItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertItem.swift; sourceTree = ""; }; D0C7D45224F76169001EBDBB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D0C7D45424F76169001EBDBB /* MetatextApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = ""; }; D0C7D45524F76169001EBDBB /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D0C7D45724F76169001EBDBB /* Localizable.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; D0C7D45824F76169001EBDBB /* Localizable.stringsdict */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; - D0C7D45A24F76169001EBDBB /* PostingReadingPreferencesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesViewModel.swift; sourceTree = ""; }; - D0C7D45B24F76169001EBDBB /* SecondaryNavigationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryNavigationViewModel.swift; sourceTree = ""; }; - D0C7D45C24F76169001EBDBB /* TabNavigationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabNavigationViewModel.swift; sourceTree = ""; }; - D0C7D45D24F76169001EBDBB /* NotificationTypesPreferencesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationTypesPreferencesViewModel.swift; sourceTree = ""; }; - D0C7D45E24F76169001EBDBB /* RootViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = ""; }; - D0C7D45F24F76169001EBDBB /* IdentitiesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentitiesViewModel.swift; sourceTree = ""; }; - D0C7D46024F76169001EBDBB /* AddIdentityViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModel.swift; sourceTree = ""; }; - D0C7D46124F76169001EBDBB /* PreferencesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesViewModel.swift; sourceTree = ""; }; - D0C7D46224F76169001EBDBB /* StatusViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusViewModel.swift; sourceTree = ""; }; - D0C7D46324F76169001EBDBB /* StatusListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusListViewModel.swift; sourceTree = ""; }; D0C7D46A24F76169001EBDBB /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+Extensions.swift"; sourceTree = ""; }; D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; - D0C7D46D24F76169001EBDBB /* Publisher+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = ""; }; D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KingfisherOptionsInfo+Extensions.swift"; sourceTree = ""; }; D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; - D0C7D47024F76169001EBDBB /* Date+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; D0C7D47124F76169001EBDBB /* Data+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = ""; }; D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = ""; }; + D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = ""; }; D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Notification Service Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D0E5362824E4A06B00FB1CE1 /* Notification Service Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Notification Service Extension.entitlements"; sourceTree = ""; }; - D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModelTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -169,9 +129,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D0BDF66724FD7CDA00C7FA1C /* ServiceLayer in Frameworks */, D06B492324D4611300642749 /* KingfisherSwiftUI in Frameworks */, - D0E2C1CE24FD7EE900854680 /* ServiceLayerMocks in Frameworks */, + D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */, + D0175CAC24FE2D6300B085F6 /* PreviewViewModels in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -179,7 +139,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D065F53924D37E5100741304 /* CombineExpectations in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -222,16 +181,14 @@ D0BFDAF524FC7C5300C86618 /* HTTP */, D0C7D45624F76169001EBDBB /* Localizations */, D0E0F1E424FC49FC002C04BF /* Mastodon */, - D0C7D43824F76169001EBDBB /* Model */, D0E5361A24E3EB4D00FB1CE1 /* Notification Service Extension */, - D0ED1BB224CE3A1600B4899C /* Preview */, D047FA8D24C3E21200AF17C5 /* Products */, D0BDF66524FD7A6400C7FA1C /* ServiceLayer */, D0C7D41D24F76169001EBDBB /* Supporting Files */, D0C7D45324F76169001EBDBB /* System */, D0666A2224C677B400F3F04B /* Tests */, D0C7D43024F76169001EBDBB /* View Controllers */, - D0C7D45924F76169001EBDBB /* View Models */, + D0E2C1CF24FD8BA400854680 /* ViewModels */, D0C7D42024F76169001EBDBB /* Views */, ); sourceTree = ""; @@ -250,7 +207,6 @@ isa = PBXGroup; children = ( D0666A2524C677B400F3F04B /* Info.plist */, - D0ED1B6C24CE0EED00B4899C /* View Models */, ); path = Tests; sourceTree = ""; @@ -302,14 +258,6 @@ path = "View Controllers"; sourceTree = ""; }; - D0C7D43824F76169001EBDBB /* Model */ = { - isa = PBXGroup; - children = ( - D0C7D45024F76169001EBDBB /* AlertItem.swift */, - ); - path = Model; - sourceTree = ""; - }; D0C7D45324F76169001EBDBB /* System */ = { isa = PBXGroup; children = ( @@ -328,35 +276,12 @@ path = Localizations; sourceTree = ""; }; - D0C7D45924F76169001EBDBB /* View Models */ = { - isa = PBXGroup; - children = ( - D0C7D46024F76169001EBDBB /* AddIdentityViewModel.swift */, - D01F41DE24F8868800D55A2D /* AttachmentViewModel.swift */, - D0BEB21224FA2C0A001B0F04 /* EditFilterViewModel.swift */, - D0BEB20624FA1121001B0F04 /* FiltersViewModel.swift */, - D0C7D45F24F76169001EBDBB /* IdentitiesViewModel.swift */, - D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */, - D0C7D45D24F76169001EBDBB /* NotificationTypesPreferencesViewModel.swift */, - D0C7D45A24F76169001EBDBB /* PostingReadingPreferencesViewModel.swift */, - D0C7D46124F76169001EBDBB /* PreferencesViewModel.swift */, - D0C7D45E24F76169001EBDBB /* RootViewModel.swift */, - D0C7D45B24F76169001EBDBB /* SecondaryNavigationViewModel.swift */, - D0C7D46324F76169001EBDBB /* StatusListViewModel.swift */, - D0C7D46224F76169001EBDBB /* StatusViewModel.swift */, - D0C7D45C24F76169001EBDBB /* TabNavigationViewModel.swift */, - ); - path = "View Models"; - sourceTree = ""; - }; D0C7D46824F76169001EBDBB /* Extensions */ = { isa = PBXGroup; children = ( D0C7D47124F76169001EBDBB /* Data+Extensions.swift */, - D0C7D47024F76169001EBDBB /* Date+Extensions.swift */, D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */, D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */, - D0C7D46D24F76169001EBDBB /* Publisher+Extensions.swift */, D0C7D46A24F76169001EBDBB /* String+Extensions.swift */, D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */, D0C7D46F24F76169001EBDBB /* View+Extensions.swift */, @@ -374,23 +299,6 @@ path = "Notification Service Extension"; sourceTree = ""; }; - D0ED1B6C24CE0EED00B4899C /* View Models */ = { - isa = PBXGroup; - children = ( - D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */, - D052BBC624D749C800A80A7A /* RootViewModelTests.swift */, - ); - path = "View Models"; - sourceTree = ""; - }; - D0ED1BB224CE3A1600B4899C /* Preview */ = { - isa = PBXGroup; - children = ( - D04FD74124D4AA34007D572D /* PreviewMocks.swift */, - ); - path = Preview; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -412,8 +320,8 @@ name = Metatext; packageProductDependencies = ( D06B492224D4611300642749 /* KingfisherSwiftUI */, - D0BDF66624FD7CDA00C7FA1C /* ServiceLayer */, - D0E2C1CD24FD7EE900854680 /* ServiceLayerMocks */, + D0E2C1D024FD97F000854680 /* ViewModels */, + D0175CAB24FE2D6300B085F6 /* PreviewViewModels */, ); productName = "Metatext (iOS)"; productReference = D047FA8C24C3E21200AF17C5 /* Metatext.app */; @@ -434,7 +342,6 @@ ); name = Tests; packageProductDependencies = ( - D065F53824D37E5100741304 /* CombineExpectations */, ); productName = "Unit Tests"; productReference = D0666A2124C677B400F3F04B /* Tests.xctest */; @@ -493,7 +400,6 @@ ); mainGroup = D047FA7F24C3E21000AF17C5; packageReferences = ( - D065F53724D37E5100741304 /* XCRemoteSwiftPackageReference "CombineExpectations" */, D06B492124D4611300642749 /* XCRemoteSwiftPackageReference "Kingfisher" */, ); productRefGroup = D047FA8D24C3E21200AF17C5 /* Products */; @@ -560,47 +466,29 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D0C7D4CA24F7616A001EBDBB /* NotificationTypesPreferencesViewModel.swift in Sources */, - D01F41DF24F8868800D55A2D /* AttachmentViewModel.swift in Sources */, D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */, D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */, - D0BEB21324FA2C0A001B0F04 /* EditFilterViewModel.swift in Sources */, - D0C7D4CD24F7616A001EBDBB /* AddIdentityViewModel.swift in Sources */, D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */, D0C7D49A24F7616A001EBDBB /* StatusListView.swift in Sources */, D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */, D0C7D4A524F7616A001EBDBB /* StatusListViewController.swift in Sources */, - D0C7D4CC24F7616A001EBDBB /* IdentitiesViewModel.swift in Sources */, - D0C7D4CB24F7616A001EBDBB /* RootViewModel.swift in Sources */, - D0C7D4CE24F7616A001EBDBB /* PreferencesViewModel.swift in Sources */, D0C7D4D624F7616A001EBDBB /* NSMutableAttributedString+Extensions.swift in Sources */, D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */, - D0C7D4D024F7616A001EBDBB /* StatusListViewModel.swift in Sources */, D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */, - D0C7D4DB24F7616A001EBDBB /* Date+Extensions.swift in Sources */, D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */, - D0C7D4C824F7616A001EBDBB /* SecondaryNavigationViewModel.swift in Sources */, D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */, - D0C7D4C024F7616A001EBDBB /* AlertItem.swift in Sources */, D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */, - D0C7D4CF24F7616A001EBDBB /* StatusViewModel.swift in Sources */, - D0C7D4C724F7616A001EBDBB /* PostingReadingPreferencesViewModel.swift in Sources */, D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */, - D04FD74224D4AA34007D572D /* PreviewMocks.swift in Sources */, D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */, D0C7D4DC24F7616A001EBDBB /* Data+Extensions.swift in Sources */, D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */, D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */, D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */, D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */, - D0BEB20724FA1121001B0F04 /* FiltersViewModel.swift in Sources */, - D0C7D4C924F7616A001EBDBB /* TabNavigationViewModel.swift in Sources */, D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */, D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */, D0C7D49924F7616A001EBDBB /* AddIdentityView.swift in Sources */, - D0BEB1FD24F9E4E5001B0F04 /* ListsViewModel.swift in Sources */, D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */, - D0C7D4D824F7616A001EBDBB /* Publisher+Extensions.swift in Sources */, D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */, D01F41D824F880C400D55A2D /* StatusTableViewCell.swift in Sources */, D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */, @@ -612,8 +500,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - D0ED1B6E24CE100C00B4899C /* AddIdentityViewModelTests.swift in Sources */, - D052BBC724D749C800A80A7A /* RootViewModelTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -760,7 +646,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "Supporting Files/Metatext.entitlements"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = Preview; + DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 82HL67AXQ2; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = "Supporting Files/Info.plist"; @@ -786,7 +672,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "Supporting Files/Metatext.entitlements"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_ASSET_PATHS = Preview; + DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 82HL67AXQ2; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = "Supporting Files/Info.plist"; @@ -942,14 +828,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - D065F53724D37E5100741304 /* XCRemoteSwiftPackageReference "CombineExpectations" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/groue/CombineExpectations"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.5.0; - }; - }; D06B492124D4611300642749 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher"; @@ -961,27 +839,22 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - D065F53824D37E5100741304 /* CombineExpectations */ = { + D0175CAB24FE2D6300B085F6 /* PreviewViewModels */ = { isa = XCSwiftPackageProductDependency; - package = D065F53724D37E5100741304 /* XCRemoteSwiftPackageReference "CombineExpectations" */; - productName = CombineExpectations; + productName = PreviewViewModels; }; D06B492224D4611300642749 /* KingfisherSwiftUI */ = { isa = XCSwiftPackageProductDependency; package = D06B492124D4611300642749 /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = KingfisherSwiftUI; }; - D0BDF66624FD7CDA00C7FA1C /* ServiceLayer */ = { - isa = XCSwiftPackageProductDependency; - productName = ServiceLayer; - }; D0BDF66A24FD7CEC00C7FA1C /* ServiceLayer */ = { isa = XCSwiftPackageProductDependency; productName = ServiceLayer; }; - D0E2C1CD24FD7EE900854680 /* ServiceLayerMocks */ = { + D0E2C1D024FD97F000854680 /* ViewModels */ = { isa = XCSwiftPackageProductDependency; - productName = ServiceLayerMocks; + productName = ViewModels; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Metatext.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Metatext.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 21a99f1..e3ec4b1 100644 --- a/Metatext.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Metatext.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -12,7 +12,7 @@ }, { "package": "CombineExpectations", - "repositoryURL": "https://github.com/groue/CombineExpectations", + "repositoryURL": "https://github.com/groue/CombineExpectations.git", "state": { "branch": null, "revision": "96d5604151c94b21fbca6877b237e80af9e821dd", diff --git a/Model/AlertItem.swift b/Model/AlertItem.swift deleted file mode 100644 index 43c3155..0000000 --- a/Model/AlertItem.swift +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright © 2020 Metabolist. All rights reserved. - -import Foundation - -struct AlertItem: Identifiable { - let id = UUID() - let error: Error -} diff --git a/Preview/PreviewMocks.swift b/Preview/PreviewMocks.swift deleted file mode 100644 index 9dace13..0000000 --- a/Preview/PreviewMocks.swift +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright © 2020 Metabolist. All rights reserved. - -import Foundation -import Combine -import HTTP -import Mastodon -import MastodonStubs -import ServiceLayer -import ServiceLayerMocks - -// swiftlint:disable force_try -private let decoder = APIDecoder() -private var cancellables = Set() -private let devInstanceURL = URL(string: "https://mastodon.social")! -private let devIdentityID = UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")! -private let devAccessToken = "DEVELOPMENT_ACCESS_TOKEN" - -extension Account { - static let development = try! decoder.decode(Account.self, - from: AccountEndpoint.verifyCredentials.data(url: devInstanceURL)!) -} - -extension Instance { - static let development = try! decoder.decode(Instance.self, - from: InstanceEndpoint.instance.data(url: devInstanceURL)!) -} - -extension AppEnvironment { - static let development = AppEnvironment( - session: Session(configuration: .stubbing), - webAuthSessionType: SuccessfulMockWebAuthSession.self, - keychainServiceType: MockKeychainService.self, - userDefaults: MockUserDefaults(), - inMemoryContent: true) -} - -extension AllIdentitiesService { - static let fresh = try! AllIdentitiesService(environment: .development) - - static var development: Self = { - let allIdentitiesService = try! AllIdentitiesService(environment: .development) - - allIdentitiesService.authorizeIdentity(id: devIdentityID, instanceURL: devInstanceURL) - .receive(on: ImmediateScheduler.shared) - .sink { _ in } receiveValue: { _ in } - .store(in: &cancellables) - -// let identityService = try! allIdentitiesService.identityService(id: devIdentityID) -// -// identityService.verifyCredentials() -// .receive(on: ImmediateScheduler.shared) -// .sink { _ in } receiveValue: { _ in } -// .store(in: &cancellables) -// -// identityService.refreshInstance() -// .receive(on: ImmediateScheduler.shared) -// .sink { _ in } receiveValue: { _ in } -// .store(in: &cancellables) - - return allIdentitiesService - } () -} - -extension IdentityService { - static let development = try! AllIdentitiesService.development.identityService(id: devIdentityID) -} - -extension UserNotificationService { - static let development = UserNotificationService(userNotificationCenter: .current()) -} - -extension RootViewModel { - static let development = RootViewModel( - appDelegate: AppDelegate(), - allIdentitiesService: .development, - userNotificationService: .development) -} - -extension AddIdentityViewModel { - static let development = RootViewModel.development.addIdentityViewModel() -} - -extension TabNavigationViewModel { - static let development = RootViewModel.development.tabNavigationViewModel! -} - -extension SecondaryNavigationViewModel { - static let development = TabNavigationViewModel.development.secondaryNavigationViewModel() -} - -extension IdentitiesViewModel { - static let development = IdentitiesViewModel(identityService: .development) -} - -extension ListsViewModel { - static let development = ListsViewModel(identityService: .development) -} - -extension PreferencesViewModel { - static let development = PreferencesViewModel(identityService: .development) -} - -extension PostingReadingPreferencesViewModel { - static let development = PostingReadingPreferencesViewModel(identityService: .development) -} - -extension NotificationTypesPreferencesViewModel { - static let development = NotificationTypesPreferencesViewModel(identityService: .development) -} - -extension FiltersViewModel { - static let development = FiltersViewModel(identityService: .development) -} - -extension EditFilterViewModel { - static let development = EditFilterViewModel(filter: Filter.new, identityService: .development) -} - -extension StatusListViewModel { - static let development = StatusListViewModel( - statusListService: IdentityService.development.service(timeline: .home)) -} - -// swiftlint:enable force_try diff --git a/ServiceLayer/Sources/ServiceLayer/AllIdentitiesService.swift b/ServiceLayer/Sources/ServiceLayer/AllIdentitiesService.swift index 7c3c360..65221b2 100644 --- a/ServiceLayer/Sources/ServiceLayer/AllIdentitiesService.swift +++ b/ServiceLayer/Sources/ServiceLayer/AllIdentitiesService.swift @@ -11,7 +11,7 @@ public struct AllIdentitiesService { private let environment: AppEnvironment public init(environment: AppEnvironment) throws { - self.identityDatabase = try IdentityDatabase(inMemory: environment.inMemoryContent) + self.identityDatabase = try IdentityDatabase(environment: environment) self.environment = environment mostRecentlyUsedIdentityID = identityDatabase.mostRecentlyUsedIdentityIDObservation() diff --git a/ServiceLayer/Sources/ServiceLayer/Database/ContentDatabase.swift b/ServiceLayer/Sources/ServiceLayer/Database/ContentDatabase.swift index 089c654..0700c81 100644 --- a/ServiceLayer/Sources/ServiceLayer/Database/ContentDatabase.swift +++ b/ServiceLayer/Sources/ServiceLayer/Database/ContentDatabase.swift @@ -9,7 +9,7 @@ import Mastodon struct ContentDatabase { private let databaseQueue: DatabaseQueue - init(identityID: UUID, inMemory: Bool) throws { + init(identityID: UUID, environment: AppEnvironment) throws { guard let documentsDirectory = NSSearchPathForDirectoriesInDomains( .documentDirectory, @@ -17,7 +17,7 @@ struct ContentDatabase { .first else { throw DatabaseError.documentsDirectoryNotFound } - if inMemory { + if environment.inMemoryContent { databaseQueue = DatabaseQueue() } else { databaseQueue = try DatabaseQueue(path: "\(documentsDirectory)/\(identityID.uuidString).sqlite3") diff --git a/ServiceLayer/Sources/ServiceLayer/Database/IdentityDatabase.swift b/ServiceLayer/Sources/ServiceLayer/Database/IdentityDatabase.swift index 55a7f89..8648ea6 100644 --- a/ServiceLayer/Sources/ServiceLayer/Database/IdentityDatabase.swift +++ b/ServiceLayer/Sources/ServiceLayer/Database/IdentityDatabase.swift @@ -12,7 +12,7 @@ enum IdentityDatabaseError: Error { struct IdentityDatabase { private let databaseQueue: DatabaseQueue - init(inMemory: Bool = false) throws { + init(environment: AppEnvironment) throws { guard let documentsDirectory = NSSearchPathForDirectoriesInDomains( .documentDirectory, @@ -20,13 +20,17 @@ struct IdentityDatabase { .first else { throw DatabaseError.documentsDirectoryNotFound } - if inMemory { + if environment.inMemoryContent { databaseQueue = DatabaseQueue() } else { databaseQueue = try DatabaseQueue(path: "\(documentsDirectory)/IdentityDatabase.sqlite3") } try Self.migrate(databaseQueue) + + if let fixture = environment.identityFixture { + try populate(fixture: fixture) + } } } @@ -235,6 +239,24 @@ private extension IdentityDatabase { try migrator.migrate(writer) } + + func populate(fixture: AppEnvironment.IdentityFixture) throws { + _ = createIdentity(id: fixture.id, url: fixture.instanceURL) + .receive(on: ImmediateScheduler.shared) + .sink { _ in } receiveValue: { _ in } + + if let instance = fixture.instance { + _ = updateInstance(instance, forIdentityID: fixture.id) + .receive(on: ImmediateScheduler.shared) + .sink { _ in } receiveValue: { _ in } + } + + if let account = fixture.account { + _ = updateAccount(account, forIdentityID: fixture.id) + .receive(on: ImmediateScheduler.shared) + .sink { _ in } receiveValue: { _ in } + } + } } private struct StoredIdentity: Codable, Hashable, FetchableRecord, PersistableRecord { diff --git a/ServiceLayer/Sources/ServiceLayer/Entities/AppEnvironment.swift b/ServiceLayer/Sources/ServiceLayer/Entities/AppEnvironment.swift index 120b4d1..c4b98a5 100644 --- a/ServiceLayer/Sources/ServiceLayer/Entities/AppEnvironment.swift +++ b/ServiceLayer/Sources/ServiceLayer/Entities/AppEnvironment.swift @@ -3,32 +3,57 @@ import Foundation import HTTP import Mastodon +import UserNotifications public struct AppEnvironment { let session: Session let webAuthSessionType: WebAuthSession.Type let keychainServiceType: KeychainService.Type let userDefaults: UserDefaults + let userNotificationClient: UserNotificationClient let inMemoryContent: Bool + let identityFixture: IdentityFixture? public init(session: Session, webAuthSessionType: WebAuthSession.Type, keychainServiceType: KeychainService.Type, userDefaults: UserDefaults, - inMemoryContent: Bool) { + userNotificationClient: UserNotificationClient, + inMemoryContent: Bool, + identityFixture: IdentityFixture?) { self.session = session self.webAuthSessionType = webAuthSessionType self.keychainServiceType = keychainServiceType self.userDefaults = userDefaults + self.userNotificationClient = userNotificationClient self.inMemoryContent = inMemoryContent + self.identityFixture = identityFixture } } public extension AppEnvironment { - static let live: Self = Self( - session: Session(configuration: .default), - webAuthSessionType: LiveWebAuthSession.self, - keychainServiceType: LiveKeychainService.self, - userDefaults: .standard, - inMemoryContent: false) + struct IdentityFixture { + public let id: UUID + public let instanceURL: URL + public let instance: Instance? + public let account: Account? + + public init(id: UUID, instanceURL: URL, instance: Instance?, account: Account?) { + self.id = id + self.instanceURL = instanceURL + self.instance = instance + self.account = account + } + } + + static func live(userNotificationCenter: UNUserNotificationCenter) -> Self { + Self( + session: Session(configuration: .default), + webAuthSessionType: LiveWebAuthSession.self, + keychainServiceType: LiveKeychainService.self, + userDefaults: .standard, + userNotificationClient: .live(userNotificationCenter), + inMemoryContent: false, + identityFixture: nil) + } } diff --git a/ServiceLayer/Sources/ServiceLayer/Entities/UserNotificationClient.swift b/ServiceLayer/Sources/ServiceLayer/Entities/UserNotificationClient.swift new file mode 100644 index 0000000..618a543 --- /dev/null +++ b/ServiceLayer/Sources/ServiceLayer/Entities/UserNotificationClient.swift @@ -0,0 +1,68 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Combine +import UserNotifications + +public struct UserNotificationClient { + public enum DelegateEvent { + case willPresentNotification(UNNotification, completionHandler: (UNNotificationPresentationOptions) -> Void) + case didReceiveResponse(UNNotificationResponse, completionHandler: () -> Void) + case openSettingsForNotification(UNNotification?) + } + + public var getNotificationSettings: (@escaping (UNNotificationSettings) -> Void) -> Void + public var requestAuthorization: (UNAuthorizationOptions, @escaping (Bool, Error?) -> Void) -> Void + public var delegateEvents: AnyPublisher + + public init( + getNotificationSettings: @escaping (@escaping (UNNotificationSettings) -> Void) -> Void, + requestAuthorization: @escaping (UNAuthorizationOptions, @escaping (Bool, Error?) -> Void) -> Void, + delegateEvents: AnyPublisher) { + self.getNotificationSettings = getNotificationSettings + self.requestAuthorization = requestAuthorization + self.delegateEvents = delegateEvents + } +} + +extension UserNotificationClient { + public static func live(_ userNotificationCenter: UNUserNotificationCenter) -> Self { + // swiftlint:disable nesting + class Delegate: NSObject, UNUserNotificationCenterDelegate { + let subject: PassthroughSubject + + init(subject: PassthroughSubject) { + self.subject = subject + } + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + subject.send(.willPresentNotification(notification, completionHandler: completionHandler)) + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + subject.send(.didReceiveResponse(response, completionHandler: completionHandler)) + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + openSettingsFor notification: UNNotification?) { + subject.send(.openSettingsForNotification(notification)) + } + } + // swiftlint:enable nesting + + let subject = PassthroughSubject() + var delegate: Delegate? = Delegate(subject: subject) + userNotificationCenter.delegate = delegate + + return UserNotificationClient( + getNotificationSettings: userNotificationCenter.getNotificationSettings, + requestAuthorization: userNotificationCenter.requestAuthorization, + delegateEvents: subject + .handleEvents(receiveCancel: { delegate = nil }) + .eraseToAnyPublisher()) + } +} diff --git a/ServiceLayer/Sources/ServiceLayer/Entities/WebAuthSession.swift b/ServiceLayer/Sources/ServiceLayer/Entities/WebAuthSession.swift index d2bee29..da10636 100644 --- a/ServiceLayer/Sources/ServiceLayer/Entities/WebAuthSession.swift +++ b/ServiceLayer/Sources/ServiceLayer/Entities/WebAuthSession.swift @@ -29,7 +29,10 @@ extension WebAuthSession { } webAuthSession.presentationContextProvider = presentationContextProvider - webAuthSession.start() + + DispatchQueue.main.async { + webAuthSession.start() + } } .eraseToAnyPublisher() } diff --git a/ServiceLayer/Sources/ServiceLayer/IdentityService.swift b/ServiceLayer/Sources/ServiceLayer/IdentityService.swift index 551ac41..db22626 100644 --- a/ServiceLayer/Sources/ServiceLayer/IdentityService.swift +++ b/ServiceLayer/Sources/ServiceLayer/IdentityService.swift @@ -39,7 +39,7 @@ public class IdentityService { networkClient.instanceURL = identity.url networkClient.accessToken = try? secretsService.item(.accessToken) - contentDatabase = try ContentDatabase(identityID: identityID, inMemory: environment.inMemoryContent) + contentDatabase = try ContentDatabase(identityID: identityID, environment: environment) observation.catch { [weak self] error -> Empty in self?.observationErrorsInput.send(error) diff --git a/ServiceLayer/Sources/ServiceLayer/UserNotificationService.swift b/ServiceLayer/Sources/ServiceLayer/UserNotificationService.swift index 265491e..1d1da8b 100644 --- a/ServiceLayer/Sources/ServiceLayer/UserNotificationService.swift +++ b/ServiceLayer/Sources/ServiceLayer/UserNotificationService.swift @@ -4,15 +4,14 @@ import Foundation import Combine import UserNotifications -public class UserNotificationService: NSObject { - private let userNotificationCenter: UNUserNotificationCenter +public struct UserNotificationService { + let events: AnyPublisher - public init(userNotificationCenter: UNUserNotificationCenter = .current()) { - self.userNotificationCenter = userNotificationCenter + private let userNotificationClient: UserNotificationClient - super.init() - - userNotificationCenter.delegate = self + public init(environment: AppEnvironment) { + self.userNotificationClient = environment.userNotificationClient + events = userNotificationClient.delegateEvents } } @@ -20,11 +19,9 @@ public extension UserNotificationService { func isAuthorized() -> AnyPublisher { getNotificationSettings() .map(\.authorizationStatus) - .flatMap { [weak self] status -> AnyPublisher in + .flatMap { status -> AnyPublisher in if status == .notDetermined { - return self?.requestProvisionalAuthorization() - .eraseToAnyPublisher() - ?? Empty().eraseToAnyPublisher() + return requestProvisionalAuthorization().eraseToAnyPublisher() } return Just(status == .authorized || status == .provisional) @@ -37,16 +34,15 @@ public extension UserNotificationService { private extension UserNotificationService { func getNotificationSettings() -> AnyPublisher { - Future { [weak self] promise in - self?.userNotificationCenter.getNotificationSettings { promise(.success($0)) } + Future { promise in + userNotificationClient.getNotificationSettings { promise(.success($0)) } } .eraseToAnyPublisher() } func requestProvisionalAuthorization() -> AnyPublisher { - Future { [weak self] promise in - self?.userNotificationCenter.requestAuthorization( - options: [.alert, .sound, .badge, .provisional]) { granted, error in + Future { promise in + userNotificationClient.requestAuthorization([.alert, .sound, .badge, .provisional]) { granted, error in if let error = error { return promise(.failure(error)) } @@ -57,13 +53,3 @@ private extension UserNotificationService { .eraseToAnyPublisher() } } - -extension UserNotificationService: UNUserNotificationCenterDelegate { - public func userNotificationCenter( - _ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - print(notification.request.content.body) - completionHandler(.banner) - } -} diff --git a/ServiceLayer/Sources/ServiceLayerMocks/MockAppEnvironment.swift b/ServiceLayer/Sources/ServiceLayerMocks/MockAppEnvironment.swift index c5e86ee..ee39981 100644 --- a/ServiceLayer/Sources/ServiceLayerMocks/MockAppEnvironment.swift +++ b/ServiceLayer/Sources/ServiceLayerMocks/MockAppEnvironment.swift @@ -1,13 +1,19 @@ +// Copyright © 2020 Metabolist. All rights reserved. + import Foundation import HTTP import ServiceLayer import Stubbing public extension AppEnvironment { - static let mock = AppEnvironment( - session: Session(configuration: .stubbing), - webAuthSessionType: SuccessfulMockWebAuthSession.self, - keychainServiceType: MockKeychainService.self, - userDefaults: MockUserDefaults(), - inMemoryContent: true) + static func mock(identityFixture: IdentityFixture? = nil) -> Self { + AppEnvironment( + session: Session(configuration: .stubbing), + webAuthSessionType: SuccessfulMockWebAuthSession.self, + keychainServiceType: MockKeychainService.self, + userDefaults: MockUserDefaults(), + userNotificationClient: .mock, + inMemoryContent: true, + identityFixture: identityFixture) + } } diff --git a/ServiceLayer/Sources/ServiceLayerMocks/MockUserNotificationClient.swift b/ServiceLayer/Sources/ServiceLayerMocks/MockUserNotificationClient.swift new file mode 100644 index 0000000..953404e --- /dev/null +++ b/ServiceLayer/Sources/ServiceLayerMocks/MockUserNotificationClient.swift @@ -0,0 +1,11 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Combine +import ServiceLayer + +public extension UserNotificationClient { + static let mock = UserNotificationClient( + getNotificationSettings: { _ in }, + requestAuthorization: { _, _ in }, + delegateEvents: Empty(completeImmediately: false).eraseToAnyPublisher()) +} diff --git a/ServiceLayer/Tests/ServiceLayerTests/AuthenticationServiceTests.swift b/ServiceLayer/Tests/ServiceLayerTests/AuthenticationServiceTests.swift index 2580870..8d34992 100644 --- a/ServiceLayer/Tests/ServiceLayerTests/AuthenticationServiceTests.swift +++ b/ServiceLayer/Tests/ServiceLayerTests/AuthenticationServiceTests.swift @@ -8,7 +8,7 @@ import CombineExpectations class AuthenticationServiceTests: XCTestCase { func testAuthentication() throws { - let sut = AuthenticationService(environment: .mock) + let sut = AuthenticationService(environment: .mock()) let instanceURL = URL(string: "https://mastodon.social")! let appAuthorizationRecorder = sut.authorizeApp(instanceURL: instanceURL).record() let appAuthorization = try wait(for: appAuthorizationRecorder.next(), timeout: 1) diff --git a/System/MetatextApp.swift b/System/MetatextApp.swift index 6d83eda..5008fa9 100644 --- a/System/MetatextApp.swift +++ b/System/MetatextApp.swift @@ -1,7 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. import SwiftUI -import ServiceLayer +import ViewModels @main struct MetatextApp: App { @@ -12,11 +12,11 @@ struct MetatextApp: App { var body: some Scene { WindowGroup { RootView( - viewModel: RootViewModel(appDelegate: appDelegate, - // swiftlint:disable force_try - allIdentitiesService: try! AllIdentitiesService(environment: .live), - // swiftlint:enable force_try - userNotificationService: UserNotificationService())) + // swiftlint:disable force_try + viewModel: try! RootViewModel( + environment: .live(userNotificationCenter: .current()), + registerForRemoteNotifications: appDelegate.registerForRemoteNotifications)) + // swiftlint:enable force_try } } } diff --git a/View Controllers/StatusListViewController.swift b/View Controllers/StatusListViewController.swift index 6a9b0d3..0f2328a 100644 --- a/View Controllers/StatusListViewController.swift +++ b/View Controllers/StatusListViewController.swift @@ -2,6 +2,7 @@ import SwiftUI import Combine +import ViewModels class StatusListViewController: UITableViewController { private let viewModel: StatusListViewModel diff --git a/ViewModels/.gitignore b/ViewModels/.gitignore new file mode 100644 index 0000000..95c4320 --- /dev/null +++ b/ViewModels/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/ViewModels/Package.swift b/ViewModels/Package.swift new file mode 100644 index 0000000..4c4b779 --- /dev/null +++ b/ViewModels/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.3 + +import PackageDescription + +let package = Package( + name: "ViewModels", + platforms: [ + .iOS(.v14), + .macOS(.v11) + ], + products: [ + .library( + name: "ViewModels", + targets: ["ViewModels"]), + .library( + name: "PreviewViewModels", + targets: ["PreviewViewModels"]) + ], + dependencies: [ + .package(url: "https://github.com/groue/CombineExpectations.git", .upToNextMajor(from: "0.5.0")), + .package(path: "ServiceLayer") + ], + targets: [ + .target( + name: "ViewModels", + dependencies: ["ServiceLayer"]), + .target( + name: "PreviewViewModels", + dependencies: ["ViewModels", .product(name: "ServiceLayerMocks", package: "ServiceLayer")]), + .testTarget( + name: "ViewModelsTests", + dependencies: ["CombineExpectations", "PreviewViewModels"]) + ] +) diff --git a/ViewModels/Sources/PreviewViewModels/ViewModelMocks.swift b/ViewModels/Sources/PreviewViewModels/ViewModelMocks.swift new file mode 100644 index 0000000..780aae6 --- /dev/null +++ b/ViewModels/Sources/PreviewViewModels/ViewModelMocks.swift @@ -0,0 +1,103 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation +import Combine +import HTTP +import Mastodon +import MastodonStubs +import ServiceLayer +import ServiceLayerMocks +import ViewModels + +private let decoder = APIDecoder() +private let devInstanceURL = URL(string: "https://mastodon.social")! + +// swiftlint:disable force_try +extension AppEnvironment { + public static let mockAuthenticated: Self = .mock( + identityFixture: .init( + id: UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!, + instanceURL: devInstanceURL, + instance: try! decoder.decode(Instance.self, + from: InstanceEndpoint.instance.data(url: devInstanceURL)!), + account: try! decoder.decode(Account.self, + from: AccountEndpoint.verifyCredentials.data(url: devInstanceURL)!))) +} + +extension RootViewModel { + public static func mock(environment: AppEnvironment = .mockAuthenticated) -> Self { + try! Self(environment: environment, + registerForRemoteNotifications: { Empty().eraseToAnyPublisher() }) + } +} +// swiftlint:enable force_try + +extension AddIdentityViewModel { + public static func mock(environment: AppEnvironment = .mockAuthenticated) -> AddIdentityViewModel { + RootViewModel.mock(environment: environment).addIdentityViewModel() + } +} + +extension TabNavigationViewModel { + public static func mock(environment: AppEnvironment = .mockAuthenticated) -> TabNavigationViewModel { + RootViewModel.mock(environment: environment).tabNavigationViewModel! + } +} + +extension SecondaryNavigationViewModel { + public static func mock(environment: AppEnvironment = .mockAuthenticated) -> SecondaryNavigationViewModel { + TabNavigationViewModel.mock(environment: environment) + .secondaryNavigationViewModel() + } +} + +extension IdentitiesViewModel { + public static func mock(environment: AppEnvironment = .mockAuthenticated) -> IdentitiesViewModel { + SecondaryNavigationViewModel.mock(environment: environment).identitiesViewModel() + } +} + +extension ListsViewModel { + public static func mock(environment: AppEnvironment = .mockAuthenticated) -> ListsViewModel { + SecondaryNavigationViewModel.mock(environment: environment).listsViewModel() + } +} + +extension PreferencesViewModel { + public static func mock(environment: AppEnvironment = .mockAuthenticated) -> PreferencesViewModel { + SecondaryNavigationViewModel.mock(environment: environment).preferencesViewModel() + } +} + +extension PostingReadingPreferencesViewModel { + public static func mock(environment: AppEnvironment = .mockAuthenticated) -> PostingReadingPreferencesViewModel { + PreferencesViewModel.mock(environment: environment) + .postingReadingPreferencesViewModel() + } +} + +extension NotificationTypesPreferencesViewModel { + public static func mock( + environment: AppEnvironment = .mockAuthenticated) -> NotificationTypesPreferencesViewModel { + PreferencesViewModel.mock(environment: environment) + .notificationTypesPreferencesViewModel() + } +} + +extension FiltersViewModel { + public static func mock(environment: AppEnvironment = .mockAuthenticated) -> FiltersViewModel { + PreferencesViewModel.mock(environment: environment).filtersViewModel() + } +} + +extension EditFilterViewModel { + public static func mock(environment: AppEnvironment = .mockAuthenticated) -> EditFilterViewModel { + FiltersViewModel.mock(environment: environment).editFilterViewModel(filter: .new) + } +} + +extension StatusListViewModel { + public static func mock(environment: AppEnvironment = .mockAuthenticated) -> StatusListViewModel { + TabNavigationViewModel.mock(environment: environment).viewModel(timeline: .home) + } +} diff --git a/View Models/AddIdentityViewModel.swift b/ViewModels/Sources/ViewModels/AddIdentityViewModel.swift similarity index 86% rename from View Models/AddIdentityViewModel.swift rename to ViewModels/Sources/ViewModels/AddIdentityViewModel.swift index f2e1fae..80752c4 100644 --- a/View Models/AddIdentityViewModel.swift +++ b/ViewModels/Sources/ViewModels/AddIdentityViewModel.swift @@ -4,11 +4,11 @@ import Foundation import Combine import ServiceLayer -class AddIdentityViewModel: ObservableObject { - @Published var urlFieldText = "" - @Published var alertItem: AlertItem? - @Published private(set) var loading = false - let addedIdentityID: AnyPublisher +public class AddIdentityViewModel: ObservableObject { + @Published public var urlFieldText = "" + @Published public var alertItem: AlertItem? + @Published public private(set) var loading = false + public let addedIdentityID: AnyPublisher private let allIdentitiesService: AllIdentitiesService private let addedIdentityIDInput = PassthroughSubject() @@ -18,7 +18,9 @@ class AddIdentityViewModel: ObservableObject { self.allIdentitiesService = allIdentitiesService addedIdentityID = addedIdentityIDInput.eraseToAnyPublisher() } +} +public extension AddIdentityViewModel { func logInTapped() { let identityID = UUID() let instanceURL: URL @@ -35,8 +37,8 @@ class AddIdentityViewModel: ObservableObject { .collect() .map { _ in (identityID, instanceURL) } .flatMap(allIdentitiesService.createIdentity(id:instanceURL:)) + .receive(on: DispatchQueue.main) .assignErrorsToAlertItem(to: \.alertItem, on: self) - .receive(on: RunLoop.main) .handleEvents( receiveSubscription: { [weak self] _ in self?.loading = true }, receiveCompletion: { [weak self] _ in self?.loading = false }) diff --git a/View Models/AttachmentViewModel.swift b/ViewModels/Sources/ViewModels/AttachmentViewModel.swift similarity index 82% rename from View Models/AttachmentViewModel.swift rename to ViewModels/Sources/ViewModels/AttachmentViewModel.swift index 0761fe3..3224883 100644 --- a/View Models/AttachmentViewModel.swift +++ b/ViewModels/Sources/ViewModels/AttachmentViewModel.swift @@ -3,15 +3,15 @@ import Foundation import Mastodon -struct AttachmentViewModel { - let attachment: Attachment +public struct AttachmentViewModel { + public let attachment: Attachment init(attachment: Attachment) { self.attachment = attachment } } -extension AttachmentViewModel { +public extension AttachmentViewModel { var aspectRatio: Double? { if let info = attachment.meta?.original, diff --git a/View Models/EditFilterViewModel.swift b/ViewModels/Sources/ViewModels/EditFilterViewModel.swift similarity index 84% rename from View Models/EditFilterViewModel.swift rename to ViewModels/Sources/ViewModels/EditFilterViewModel.swift index 1f08b7c..7b4f6b3 100644 --- a/View Models/EditFilterViewModel.swift +++ b/ViewModels/Sources/ViewModels/EditFilterViewModel.swift @@ -5,13 +5,13 @@ import Combine import Mastodon import ServiceLayer -class EditFilterViewModel: ObservableObject { - @Published var filter: Filter - @Published var saving = false - @Published var alertItem: AlertItem? - let saveCompleted: AnyPublisher +public class EditFilterViewModel: ObservableObject { + @Published public var filter: Filter + @Published public var saving = false + @Published public var alertItem: AlertItem? + public let saveCompleted: AnyPublisher - var date: Date { + public var date: Date { didSet { filter.expiresAt = date } } @@ -27,7 +27,7 @@ class EditFilterViewModel: ObservableObject { } } -extension EditFilterViewModel { +public extension EditFilterViewModel { var isNew: Bool { filter.id == Filter.newFilterID } var isSaveDisabled: Bool { filter.phrase == "" || filter.context.isEmpty } diff --git a/ViewModels/Sources/ViewModels/Entities/AlertItem.swift b/ViewModels/Sources/ViewModels/Entities/AlertItem.swift new file mode 100644 index 0000000..8a864b5 --- /dev/null +++ b/ViewModels/Sources/ViewModels/Entities/AlertItem.swift @@ -0,0 +1,8 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +public struct AlertItem: Identifiable { + public let id = UUID() + public let error: Error +} diff --git a/Extensions/Date+Extensions.swift b/ViewModels/Sources/ViewModels/Extensions/Date+Extensions.swift similarity index 100% rename from Extensions/Date+Extensions.swift rename to ViewModels/Sources/ViewModels/Extensions/Date+Extensions.swift diff --git a/Extensions/Publisher+Extensions.swift b/ViewModels/Sources/ViewModels/Extensions/Publisher+Extensions.swift similarity index 100% rename from Extensions/Publisher+Extensions.swift rename to ViewModels/Sources/ViewModels/Extensions/Publisher+Extensions.swift diff --git a/ViewModels/Sources/ViewModels/Extensions/String+Extensions.swift b/ViewModels/Sources/ViewModels/Extensions/String+Extensions.swift new file mode 100644 index 0000000..116bb48 --- /dev/null +++ b/ViewModels/Sources/ViewModels/Extensions/String+Extensions.swift @@ -0,0 +1,21 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +extension String { + private static let HTTPSPrefix = "https://" + + func url() throws -> URL { + let url: URL? + + if hasPrefix(Self.HTTPSPrefix) { + url = URL(string: self) + } else { + url = URL(string: Self.HTTPSPrefix + self) + } + + guard let validURL = url else { throw URLError(.badURL) } + + return validURL + } +} diff --git a/View Models/FiltersViewModel.swift b/ViewModels/Sources/ViewModels/FiltersViewModel.swift similarity index 84% rename from View Models/FiltersViewModel.swift rename to ViewModels/Sources/ViewModels/FiltersViewModel.swift index cfe522c..ea34550 100644 --- a/View Models/FiltersViewModel.swift +++ b/ViewModels/Sources/ViewModels/FiltersViewModel.swift @@ -5,10 +5,10 @@ import Combine import Mastodon import ServiceLayer -class FiltersViewModel: ObservableObject { - @Published var activeFilters = [Filter]() - @Published var expiredFilters = [Filter]() - @Published var alertItem: AlertItem? +public class FiltersViewModel: ObservableObject { + @Published public var activeFilters = [Filter]() + @Published public var expiredFilters = [Filter]() + @Published public var alertItem: AlertItem? private let identityService: IdentityService private var cancellables = Set() @@ -28,7 +28,7 @@ class FiltersViewModel: ObservableObject { } } -extension FiltersViewModel { +public extension FiltersViewModel { func refreshFilters() { identityService.refreshFilters() .assignErrorsToAlertItem(to: \.alertItem, on: self) diff --git a/View Models/IdentitiesViewModel.swift b/ViewModels/Sources/ViewModels/IdentitiesViewModel.swift similarity index 70% rename from View Models/IdentitiesViewModel.swift rename to ViewModels/Sources/ViewModels/IdentitiesViewModel.swift index de7db6f..ac84e87 100644 --- a/View Models/IdentitiesViewModel.swift +++ b/ViewModels/Sources/ViewModels/IdentitiesViewModel.swift @@ -4,10 +4,10 @@ import Combine import Foundation import ServiceLayer -class IdentitiesViewModel: ObservableObject { - @Published private(set) var identity: Identity - @Published var identities = [Identity]() - @Published var alertItem: AlertItem? +public class IdentitiesViewModel: ObservableObject { + @Published public private(set) var identity: Identity + @Published public var identities = [Identity]() + @Published public var alertItem: AlertItem? private let identityService: IdentityService private var cancellables = Set() diff --git a/View Models/ListsViewModel.swift b/ViewModels/Sources/ViewModels/ListsViewModel.swift similarity index 85% rename from View Models/ListsViewModel.swift rename to ViewModels/Sources/ViewModels/ListsViewModel.swift index d1cd101..fcc2f3f 100644 --- a/View Models/ListsViewModel.swift +++ b/ViewModels/Sources/ViewModels/ListsViewModel.swift @@ -5,10 +5,10 @@ import Combine import Mastodon import ServiceLayer -class ListsViewModel: ObservableObject { - @Published private(set) var lists = [MastodonList]() - @Published private(set) var creatingList = false - @Published var alertItem: AlertItem? +public class ListsViewModel: ObservableObject { + @Published public private(set) var lists = [MastodonList]() + @Published public private(set) var creatingList = false + @Published public var alertItem: AlertItem? private let identityService: IdentityService private var cancellables = Set() @@ -29,7 +29,7 @@ class ListsViewModel: ObservableObject { } } -extension ListsViewModel { +public extension ListsViewModel { func refreshLists() { identityService.refreshLists() .assignErrorsToAlertItem(to: \.alertItem, on: self) diff --git a/View Models/NotificationTypesPreferencesViewModel.swift b/ViewModels/Sources/ViewModels/NotificationTypesPreferencesViewModel.swift similarity index 87% rename from View Models/NotificationTypesPreferencesViewModel.swift rename to ViewModels/Sources/ViewModels/NotificationTypesPreferencesViewModel.swift index b3ad906..a246a02 100644 --- a/View Models/NotificationTypesPreferencesViewModel.swift +++ b/ViewModels/Sources/ViewModels/NotificationTypesPreferencesViewModel.swift @@ -5,9 +5,9 @@ import Combine import Mastodon import ServiceLayer -class NotificationTypesPreferencesViewModel: ObservableObject { - @Published var pushSubscriptionAlerts: PushSubscription.Alerts - @Published var alertItem: AlertItem? +public class NotificationTypesPreferencesViewModel: ObservableObject { + @Published public var pushSubscriptionAlerts: PushSubscription.Alerts + @Published public var alertItem: AlertItem? private let identityService: IdentityService private var cancellables = Set() diff --git a/View Models/PostingReadingPreferencesViewModel.swift b/ViewModels/Sources/ViewModels/PostingReadingPreferencesViewModel.swift similarity index 81% rename from View Models/PostingReadingPreferencesViewModel.swift rename to ViewModels/Sources/ViewModels/PostingReadingPreferencesViewModel.swift index 61c4675..e80a17e 100644 --- a/View Models/PostingReadingPreferencesViewModel.swift +++ b/ViewModels/Sources/ViewModels/PostingReadingPreferencesViewModel.swift @@ -4,9 +4,9 @@ import Foundation import Combine import ServiceLayer -class PostingReadingPreferencesViewModel: ObservableObject { - @Published var preferences: Identity.Preferences - @Published var alertItem: AlertItem? +public class PostingReadingPreferencesViewModel: ObservableObject { + @Published public var preferences: Identity.Preferences + @Published public var alertItem: AlertItem? private let identityService: IdentityService private var cancellables = Set() diff --git a/View Models/PreferencesViewModel.swift b/ViewModels/Sources/ViewModels/PreferencesViewModel.swift similarity index 82% rename from View Models/PreferencesViewModel.swift rename to ViewModels/Sources/ViewModels/PreferencesViewModel.swift index 8b40364..c5564c9 100644 --- a/View Models/PreferencesViewModel.swift +++ b/ViewModels/Sources/ViewModels/PreferencesViewModel.swift @@ -3,9 +3,9 @@ import Foundation import ServiceLayer -class PreferencesViewModel: ObservableObject { - let handle: String - let shouldShowNotificationTypePreferences: Bool +public class PreferencesViewModel: ObservableObject { + public let handle: String + public let shouldShowNotificationTypePreferences: Bool private let identityService: IdentityService @@ -17,7 +17,7 @@ class PreferencesViewModel: ObservableObject { } } -extension PreferencesViewModel { +public extension PreferencesViewModel { func postingReadingPreferencesViewModel() -> PostingReadingPreferencesViewModel { PostingReadingPreferencesViewModel(identityService: identityService) } diff --git a/View Models/RootViewModel.swift b/ViewModels/Sources/ViewModels/RootViewModel.swift similarity index 75% rename from View Models/RootViewModel.swift rename to ViewModels/Sources/ViewModels/RootViewModel.swift index c1b4d35..967827d 100644 --- a/View Models/RootViewModel.swift +++ b/ViewModels/Sources/ViewModels/RootViewModel.swift @@ -4,23 +4,20 @@ import Foundation import Combine import ServiceLayer -class RootViewModel: ObservableObject { - @Published private(set) var tabNavigationViewModel: TabNavigationViewModel? - @Published private var mostRecentlyUsedIdentityID: UUID? +public final class RootViewModel: ObservableObject { + @Published public private(set) var tabNavigationViewModel: TabNavigationViewModel? - // swiftlint:disable weak_delegate - private let appDelegate: AppDelegate - // swiftlint:enable weak_delegate + @Published private var mostRecentlyUsedIdentityID: UUID? private let allIdentitiesService: AllIdentitiesService private let userNotificationService: UserNotificationService + private let registerForRemoteNotifications: () -> AnyPublisher private var cancellables = Set() - init(appDelegate: AppDelegate, - allIdentitiesService: AllIdentitiesService, - userNotificationService: UserNotificationService) { - self.appDelegate = appDelegate - self.allIdentitiesService = allIdentitiesService - self.userNotificationService = userNotificationService + public init(environment: AppEnvironment, + registerForRemoteNotifications: @escaping () -> AnyPublisher) throws { + allIdentitiesService = try AllIdentitiesService(environment: environment) + userNotificationService = UserNotificationService(environment: environment) + self.registerForRemoteNotifications = registerForRemoteNotifications allIdentitiesService.mostRecentlyUsedIdentityID.assign(to: &$mostRecentlyUsedIdentityID) @@ -28,7 +25,7 @@ class RootViewModel: ObservableObject { userNotificationService.isAuthorized() .filter { $0 } - .zip(appDelegate.registerForRemoteNotifications()) + .zip(registerForRemoteNotifications()) .map { $1 } .flatMap(allIdentitiesService.updatePushSubscriptions(deviceToken:)) .sink { _ in } receiveValue: { _ in } @@ -36,7 +33,7 @@ class RootViewModel: ObservableObject { } } -extension RootViewModel { +public extension RootViewModel { func newIdentitySelected(id: UUID?) { guard let id = id else { tabNavigationViewModel = nil @@ -64,7 +61,7 @@ extension RootViewModel { userNotificationService.isAuthorized() .filter { $0 } - .zip(appDelegate.registerForRemoteNotifications()) + .zip(registerForRemoteNotifications()) .filter { identityService.identity.lastRegisteredDeviceToken != $1 } .map { ($1, identityService.identity.pushSubscriptionAlerts) } .flatMap(identityService.createPushSubscription(deviceToken:alerts:)) diff --git a/View Models/SecondaryNavigationViewModel.swift b/ViewModels/Sources/ViewModels/SecondaryNavigationViewModel.swift similarity index 81% rename from View Models/SecondaryNavigationViewModel.swift rename to ViewModels/Sources/ViewModels/SecondaryNavigationViewModel.swift index da9aba3..fd00ead 100644 --- a/View Models/SecondaryNavigationViewModel.swift +++ b/ViewModels/Sources/ViewModels/SecondaryNavigationViewModel.swift @@ -3,8 +3,9 @@ import Foundation import ServiceLayer -class SecondaryNavigationViewModel: ObservableObject { - @Published private(set) var identity: Identity +public class SecondaryNavigationViewModel: ObservableObject { + @Published public private(set) var identity: Identity + private let identityService: IdentityService init(identityService: IdentityService) { @@ -14,7 +15,7 @@ class SecondaryNavigationViewModel: ObservableObject { } } -extension SecondaryNavigationViewModel { +public extension SecondaryNavigationViewModel { func identitiesViewModel() -> IdentitiesViewModel { IdentitiesViewModel(identityService: identityService) } diff --git a/View Models/StatusListViewModel.swift b/ViewModels/Sources/ViewModels/StatusListViewModel.swift similarity index 90% rename from View Models/StatusListViewModel.swift rename to ViewModels/Sources/ViewModels/StatusListViewModel.swift index 4237491..88e2d6d 100644 --- a/View Models/StatusListViewModel.swift +++ b/ViewModels/Sources/ViewModels/StatusListViewModel.swift @@ -5,11 +5,11 @@ import Combine import Mastodon import ServiceLayer -class StatusListViewModel: ObservableObject { - @Published private(set) var statusIDs = [[String]]() - @Published var alertItem: AlertItem? - @Published private(set) var loading = false - private(set) var maintainScrollPositionOfStatusID: String? +public class StatusListViewModel: ObservableObject { + @Published public private(set) var statusIDs = [[String]]() + @Published public var alertItem: AlertItem? + @Published public private(set) var loading = false + public private(set) var maintainScrollPositionOfStatusID: String? private var statuses = [String: Status]() private let statusListService: StatusListService @@ -27,19 +27,21 @@ class StatusListViewModel: ObservableObject { self?.cleanViewModelCache(newStatusSections: $0) self?.statuses = Dictionary(uniqueKeysWithValues: $0.reduce([], +).map { ($0.id, $0) }) }) + .receive(on: DispatchQueue.main) .assignErrorsToAlertItem(to: \.alertItem, on: self) .map { $0.map { $0.map(\.id) } } .assign(to: &$statusIDs) } } -extension StatusListViewModel { +public extension StatusListViewModel { var paginates: Bool { statusListService.paginates } var contextParentID: String? { statusListService.contextParentID } func request(maxID: String? = nil, minID: String? = nil) { statusListService.request(maxID: maxID, minID: minID) + .receive(on: DispatchQueue.main) .assignErrorsToAlertItem(to: \.alertItem, on: self) .handleEvents( receiveSubscription: { [weak self] _ in self?.loading = true }, diff --git a/View Models/StatusViewModel.swift b/ViewModels/Sources/ViewModels/StatusViewModel.swift similarity index 82% rename from View Models/StatusViewModel.swift rename to ViewModels/Sources/ViewModels/StatusViewModel.swift index 87dc60e..cb9a612 100644 --- a/View Models/StatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/StatusViewModel.swift @@ -5,24 +5,24 @@ import Combine import Mastodon import ServiceLayer -struct StatusViewModel { - let content: NSAttributedString - let contentEmoji: [Emoji] - let displayName: String - let displayNameEmoji: [Emoji] - let spoilerText: String - let isReblog: Bool - let rebloggedByDisplayName: String - let rebloggedByDisplayNameEmoji: [Emoji] - let attachmentViewModels: [AttachmentViewModel] - let pollOptionTitles: [String] - let pollEmoji: [Emoji] - var isPinned = false - var isContextParent = false - var isReplyInContext = false - var hasReplyFollowing = false - var sensitiveContentToggled = false - let events: AnyPublisher, Never> +public struct StatusViewModel { + public let content: NSAttributedString + public let contentEmoji: [Emoji] + public let displayName: String + public let displayNameEmoji: [Emoji] + public let spoilerText: String + public let isReblog: Bool + public let rebloggedByDisplayName: String + public let rebloggedByDisplayNameEmoji: [Emoji] + public let attachmentViewModels: [AttachmentViewModel] + public let pollOptionTitles: [String] + public let pollEmoji: [Emoji] + public var isPinned = false + public var isContextParent = false + public var isReplyInContext = false + public var hasReplyFollowing = false + public var sensitiveContentToggled = false + public let events: AnyPublisher, Never> private let statusService: StatusService private let eventsInput = PassthroughSubject, Never>() @@ -49,7 +49,7 @@ struct StatusViewModel { } } -extension StatusViewModel { +public extension StatusViewModel { var shouldDisplaySensitiveContent: Bool { if statusService.status.displayStatus.sensitive { return sensitiveContentToggled diff --git a/View Models/TabNavigationViewModel.swift b/ViewModels/Sources/ViewModels/TabNavigationViewModel.swift similarity index 73% rename from View Models/TabNavigationViewModel.swift rename to ViewModels/Sources/ViewModels/TabNavigationViewModel.swift index e6db9c4..c123eb8 100644 --- a/View Models/TabNavigationViewModel.swift +++ b/ViewModels/Sources/ViewModels/TabNavigationViewModel.swift @@ -5,14 +5,14 @@ import Combine import Mastodon import ServiceLayer -class TabNavigationViewModel: ObservableObject { - @Published private(set) var identity: Identity - @Published private(set) var recentIdentities = [Identity]() - @Published var timeline = Timeline.home - @Published private(set) var timelinesAndLists = Timeline.nonLists - @Published var presentingSecondaryNavigation = false - @Published var alertItem: AlertItem? - var selectedTab: Tab? = .timelines +public class TabNavigationViewModel: ObservableObject { + @Published public private(set) var identity: Identity + @Published public private(set) var recentIdentities = [Identity]() + @Published public var timeline = Timeline.home + @Published public private(set) var timelinesAndLists = Timeline.nonLists + @Published public var presentingSecondaryNavigation = false + @Published public var alertItem: AlertItem? + public var selectedTab: Tab? = .timelines private let identityService: IdentityService private var cancellables = Set() @@ -33,7 +33,7 @@ class TabNavigationViewModel: ObservableObject { } } -extension TabNavigationViewModel { +public extension TabNavigationViewModel { var timelineSubtitle: String { switch timeline { case .home, .list: @@ -93,7 +93,7 @@ extension TabNavigationViewModel { } } -extension TabNavigationViewModel { +public extension TabNavigationViewModel { enum Tab: CaseIterable { case timelines case search @@ -102,26 +102,6 @@ extension TabNavigationViewModel { } } -extension TabNavigationViewModel.Tab { - var title: String { - switch self { - case .timelines: return "Timelines" - case .search: return "Search" - case .notifications: return "Notifications" - case .messages: return "Messages" - } - } - - var systemImageName: String { - switch self { - case .timelines: return "newspaper" - case .search: return "magnifyingglass" - case .notifications: return "bell" - case .messages: return "envelope" - } - } -} - extension TabNavigationViewModel.Tab: Identifiable { - var id: Self { self } + public var id: Self { self } } diff --git a/Tests/View Models/AddIdentityViewModelTests.swift b/ViewModels/Tests/ViewModelsTests/AddIdentityViewModelTests.swift similarity index 78% rename from Tests/View Models/AddIdentityViewModelTests.swift rename to ViewModels/Tests/ViewModelsTests/AddIdentityViewModelTests.swift index 31008a8..7dadee0 100644 --- a/Tests/View Models/AddIdentityViewModelTests.swift +++ b/ViewModels/Tests/ViewModelsTests/AddIdentityViewModelTests.swift @@ -5,13 +5,13 @@ import Combine import CombineExpectations import HTTP import Mastodon -@testable import Metatext - import ServiceLayer +import ServiceLayerMocks +@testable import ViewModels class AddIdentityViewModelTests: XCTestCase { func testAddIdentity() throws { - let sut = AddIdentityViewModel(allIdentitiesService: .fresh) + let sut = AddIdentityViewModel(allIdentitiesService: try AllIdentitiesService(environment: .mock())) let addedIDRecorder = sut.addedIdentityID.record() sut.urlFieldText = "https://mastodon.social" @@ -21,7 +21,7 @@ class AddIdentityViewModelTests: XCTestCase { } func testAddIdentityWithoutScheme() throws { - let sut = AddIdentityViewModel(allIdentitiesService: .fresh) + let sut = AddIdentityViewModel(allIdentitiesService: try AllIdentitiesService(environment: .mock())) let addedIDRecorder = sut.addedIdentityID.record() sut.urlFieldText = "mastodon.social" @@ -31,7 +31,7 @@ class AddIdentityViewModelTests: XCTestCase { } func testInvalidURL() throws { - let sut = AddIdentityViewModel(allIdentitiesService: .fresh) + let sut = AddIdentityViewModel(allIdentitiesService: try AllIdentitiesService(environment: .mock())) let recorder = sut.$alertItem.record() XCTAssertNil(try wait(for: recorder.next(), timeout: 1)) @@ -50,7 +50,9 @@ class AddIdentityViewModelTests: XCTestCase { webAuthSessionType: CanceledLoginMockWebAuthSession.self, keychainServiceType: MockKeychainService.self, userDefaults: MockUserDefaults(), - inMemoryContent: true) + userNotificationClient: .mock, + inMemoryContent: true, + identityFixture: nil) let allIdentitiesService = try AllIdentitiesService(environment: environment) let sut = AddIdentityViewModel(allIdentitiesService: allIdentitiesService) let recorder = sut.$alertItem.record() @@ -62,8 +64,4 @@ class AddIdentityViewModelTests: XCTestCase { try wait(for: recorder.next().inverted, timeout: 1) } - - func testFuck() { - - } } diff --git a/Tests/View Models/RootViewModelTests.swift b/ViewModels/Tests/ViewModelsTests/RootViewModelTests.swift similarity index 78% rename from Tests/View Models/RootViewModelTests.swift rename to ViewModels/Tests/ViewModelsTests/RootViewModelTests.swift index 0e5a5f3..3a8a182 100644 --- a/Tests/View Models/RootViewModelTests.swift +++ b/ViewModels/Tests/ViewModelsTests/RootViewModelTests.swift @@ -4,15 +4,16 @@ import XCTest import Combine import CombineExpectations import ServiceLayer -@testable import Metatext +import ServiceLayerMocks +@testable import ViewModels class RootViewModelTests: XCTestCase { var cancellables = Set() func testAddIdentity() throws { - let sut = RootViewModel(appDelegate: AppDelegate(), - allIdentitiesService: .fresh, - userNotificationService: UserNotificationService()) + let sut = try RootViewModel( + environment: .mock(), + registerForRemoteNotifications: { Empty().setFailureType(to: Error.self).eraseToAnyPublisher() }) let recorder = sut.$tabNavigationViewModel.record() XCTAssertNil(try wait(for: recorder.next(), timeout: 1)) diff --git a/Views/AddIdentityView.swift b/Views/AddIdentityView.swift index dfcce47..e6e149c 100644 --- a/Views/AddIdentityView.swift +++ b/Views/AddIdentityView.swift @@ -1,6 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. import SwiftUI +import ViewModels struct AddIdentityView: View { @StateObject var viewModel: AddIdentityViewModel @@ -40,9 +41,11 @@ extension AddIdentityView { } #if DEBUG +import PreviewViewModels + struct AddAccountView_Previews: PreviewProvider { static var previews: some View { - AddIdentityView(viewModel: .development) + AddIdentityView(viewModel: .mock()) } } #endif diff --git a/Views/Attachments/AttachmentView.swift b/Views/Attachments/AttachmentView.swift index 5fc23c4..ef6d7b2 100644 --- a/Views/Attachments/AttachmentView.swift +++ b/Views/Attachments/AttachmentView.swift @@ -2,6 +2,7 @@ import UIKit import Kingfisher +import ViewModels class AttachmentView: UIView { let imageView = AnimatedImageView() diff --git a/Views/Attachments/AttachmentsView.swift b/Views/Attachments/AttachmentsView.swift index c72fbaf..7540b00 100644 --- a/Views/Attachments/AttachmentsView.swift +++ b/Views/Attachments/AttachmentsView.swift @@ -1,6 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. import UIKit +import ViewModels class AttachmentsView: UIView { private let containerStackView = UIStackView() diff --git a/Views/EditFilterView.swift b/Views/EditFilterView.swift index 711fc74..116d8b4 100644 --- a/Views/EditFilterView.swift +++ b/Views/EditFilterView.swift @@ -2,6 +2,7 @@ import SwiftUI import struct Mastodon.Filter +import ViewModels struct EditFilterView: View { @StateObject var viewModel: EditFilterViewModel @@ -96,9 +97,11 @@ extension Filter.Context { } #if DEBUG +import PreviewViewModels + struct EditFilterView_Previews: PreviewProvider { static var previews: some View { - EditFilterView(viewModel: .development) + EditFilterView(viewModel: .mock()) } } #endif diff --git a/Views/FiltersView.swift b/Views/FiltersView.swift index 040c5ec..96a24b6 100644 --- a/Views/FiltersView.swift +++ b/Views/FiltersView.swift @@ -2,6 +2,7 @@ import SwiftUI import struct Mastodon.Filter +import ViewModels struct FiltersView: View { @StateObject var viewModel: FiltersViewModel @@ -55,9 +56,11 @@ private extension FiltersView { } #if DEBUG +import PreviewViewModels + struct FiltersView_Previews: PreviewProvider { static var previews: some View { - FiltersView(viewModel: .development) + FiltersView(viewModel: .mock()) } } #endif diff --git a/Views/IdentitiesView.swift b/Views/IdentitiesView.swift index 956fb64..28a4142 100644 --- a/Views/IdentitiesView.swift +++ b/Views/IdentitiesView.swift @@ -2,7 +2,7 @@ import SwiftUI import KingfisherSwiftUI -import struct ServiceLayer.Identity +import ViewModels struct IdentitiesView: View { @StateObject var viewModel: IdentitiesViewModel @@ -68,10 +68,12 @@ struct IdentitiesView: View { } #if DEBUG +import PreviewViewModels + struct IdentitiesView_Previews: PreviewProvider { static var previews: some View { - IdentitiesView(viewModel: .development) - .environmentObject(RootViewModel.development) + IdentitiesView(viewModel: .mock()) + .environmentObject(RootViewModel.mock()) } } #endif diff --git a/Views/ListsView.swift b/Views/ListsView.swift index 9c220e4..8d4c5e1 100644 --- a/Views/ListsView.swift +++ b/Views/ListsView.swift @@ -1,6 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. import SwiftUI +import ViewModels struct ListsView: View { @StateObject var viewModel: ListsViewModel @@ -55,10 +56,12 @@ struct ListsView: View { } #if DEBUG +import PreviewViewModels + struct ListsView_Previews: PreviewProvider { static var previews: some View { - ListsView(viewModel: .development) - .environmentObject(TabNavigationViewModel.development) + ListsView(viewModel: .mock()) + .environmentObject(TabNavigationViewModel.mock()) } } #endif diff --git a/Views/NotificationTypesPreferencesView.swift b/Views/NotificationTypesPreferencesView.swift index 9962a26..f2a792b 100644 --- a/Views/NotificationTypesPreferencesView.swift +++ b/Views/NotificationTypesPreferencesView.swift @@ -1,6 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. import SwiftUI +import ViewModels struct NotificationTypesPreferencesView: View { @StateObject var viewModel: NotificationTypesPreferencesViewModel @@ -24,9 +25,11 @@ struct NotificationTypesPreferencesView: View { } #if DEBUG +import PreviewViewModels + struct NotificationTypesPreferencesView_Previews: PreviewProvider { static var previews: some View { - NotificationTypesPreferencesView(viewModel: .development) + NotificationTypesPreferencesView(viewModel: .mock()) } } #endif diff --git a/Views/PostingReadingPreferencesView.swift b/Views/PostingReadingPreferencesView.swift index c1b86d9..4784312 100644 --- a/Views/PostingReadingPreferencesView.swift +++ b/Views/PostingReadingPreferencesView.swift @@ -3,6 +3,7 @@ import SwiftUI import class Mastodon.Status import struct Mastodon.Preferences +import ViewModels struct PostingReadingPreferencesView: View { @StateObject var viewModel: PostingReadingPreferencesViewModel @@ -50,9 +51,11 @@ struct PostingReadingPreferencesView: View { } #if DEBUG +import PreviewViewModels + struct PostingReadingPreferencesViewView_Previews: PreviewProvider { static var previews: some View { - PostingReadingPreferencesView(viewModel: .development) + PostingReadingPreferencesView(viewModel: .mock()) } } #endif diff --git a/Views/PreferencesView.swift b/Views/PreferencesView.swift index d8a1edc..47d8bc6 100644 --- a/Views/PreferencesView.swift +++ b/Views/PreferencesView.swift @@ -1,6 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. import SwiftUI +import ViewModels struct PreferencesView: View { @StateObject var viewModel: PreferencesViewModel @@ -26,9 +27,11 @@ struct PreferencesView: View { } #if DEBUG +import PreviewViewModels + struct PreferencesView_Previews: PreviewProvider { static var previews: some View { - PreferencesView(viewModel: .development) + PreferencesView(viewModel: .mock()) } } #endif diff --git a/Views/RootView.swift b/Views/RootView.swift index 9719a92..19518f2 100644 --- a/Views/RootView.swift +++ b/Views/RootView.swift @@ -1,6 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. import SwiftUI +import ViewModels struct RootView: View { @StateObject var viewModel: RootViewModel @@ -20,9 +21,11 @@ struct RootView: View { } #if DEBUG +import PreviewViewModels + struct ContentView_Previews: PreviewProvider { static var previews: some View { - RootView(viewModel: .development) + RootView(viewModel: .mock()) } } #endif diff --git a/Views/SecondaryNavigationView.swift b/Views/SecondaryNavigationView.swift index c5ac196..faae71a 100644 --- a/Views/SecondaryNavigationView.swift +++ b/Views/SecondaryNavigationView.swift @@ -2,6 +2,7 @@ import SwiftUI import KingfisherSwiftUI +import ViewModels struct SecondaryNavigationView: View { @StateObject var viewModel: SecondaryNavigationViewModel @@ -66,11 +67,13 @@ struct SecondaryNavigationView: View { } #if DEBUG +import PreviewViewModels + struct SecondaryNavigationView_Previews: PreviewProvider { static var previews: some View { - SecondaryNavigationView(viewModel: .development) - .environmentObject(RootViewModel.development) - .environmentObject(TabNavigationViewModel.development) + SecondaryNavigationView(viewModel: .mock()) + .environmentObject(RootViewModel.mock()) + .environmentObject(TabNavigationViewModel.mock()) } } #endif diff --git a/Views/Status Cell/StatusTableViewCell.swift b/Views/Status Cell/StatusTableViewCell.swift index f0b7896..12661c8 100644 --- a/Views/Status Cell/StatusTableViewCell.swift +++ b/Views/Status Cell/StatusTableViewCell.swift @@ -3,6 +3,7 @@ import AVKit import Kingfisher import UIKit +import ViewModels protocol StatusTableViewCellDelegate: class { func statusTableViewCellDidHaveShareButtonTapped(_ cell: StatusTableViewCell) diff --git a/Views/StatusListView.swift b/Views/StatusListView.swift index e2cd891..43f5346 100644 --- a/Views/StatusListView.swift +++ b/Views/StatusListView.swift @@ -1,6 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. import SwiftUI +import ViewModels struct StatusListView: UIViewControllerRepresentable { let viewModel: StatusListViewModel @@ -15,9 +16,11 @@ struct StatusListView: UIViewControllerRepresentable { } #if DEBUG +import PreviewViewModels + struct StatusListView_Previews: PreviewProvider { static var previews: some View { - StatusListView(viewModel: .development) + StatusListView(viewModel: .mock()) } } #endif diff --git a/Views/TabNavigationView.swift b/Views/TabNavigationView.swift index da19148..7af483d 100644 --- a/Views/TabNavigationView.swift +++ b/Views/TabNavigationView.swift @@ -2,6 +2,7 @@ import SwiftUI import KingfisherSwiftUI +import ViewModels struct TabNavigationView: View { @ObservedObject var viewModel: TabNavigationViewModel @@ -118,11 +119,33 @@ private extension TabNavigationView { } } +extension TabNavigationViewModel.Tab { + var title: String { + switch self { + case .timelines: return "Timelines" + case .search: return "Search" + case .notifications: return "Notifications" + case .messages: return "Messages" + } + } + + var systemImageName: String { + switch self { + case .timelines: return "newspaper" + case .search: return "magnifyingglass" + case .notifications: return "bell" + case .messages: return "envelope" + } + } +} + #if DEBUG +import PreviewViewModels + struct TabNavigation_Previews: PreviewProvider { static var previews: some View { - TabNavigationView(viewModel: .development) - .environmentObject(RootViewModel.development) + TabNavigationView(viewModel: .mock()) + .environmentObject(RootViewModel.mock()) } } #endif