Modularize view models

This commit is contained in:
Justin Mazzocchi 2020-09-01 00:33:49 -07:00
parent bd1f7af036
commit 038385c2c7
No known key found for this signature in database
GPG Key ID: E223E6937AAFB01C
60 changed files with 561 additions and 491 deletions

View File

@ -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))

View File

@ -2,6 +2,7 @@
import Foundation
import SwiftUI
import ViewModels
extension View {
func alertItem(_ alertItem: Binding<AlertItem?>) -> some View {

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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 = "<group>"; };
D01F41D524F880C400D55A2D /* StatusTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = "<group>"; };
D01F41DE24F8868800D55A2D /* AttachmentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentViewModel.swift; sourceTree = "<group>"; };
D01F41E224F8889700D55A2D /* AttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentsView.swift; sourceTree = "<group>"; };
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 = "<group>"; };
D052BBC624D749C800A80A7A /* RootViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModelTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
D0BDF66524FD7A6400C7FA1C /* ServiceLayer */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ServiceLayer; sourceTree = "<group>"; };
D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableFooterView.swift; sourceTree = "<group>"; };
D0BEB1FC24F9E4E5001B0F04 /* ListsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsViewModel.swift; sourceTree = "<group>"; };
D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsView.swift; sourceTree = "<group>"; };
D0BEB20424FA1107001B0F04 /* FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersView.swift; sourceTree = "<group>"; };
D0BEB20624FA1121001B0F04 /* FiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewModel.swift; sourceTree = "<group>"; };
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterView.swift; sourceTree = "<group>"; };
D0BEB21224FA2C0A001B0F04 /* EditFilterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFilterViewModel.swift; sourceTree = "<group>"; };
D0BFDAF524FC7C5300C86618 /* HTTP */ = {isa = PBXFileReference; lastKnownFileType = folder; path = HTTP; sourceTree = "<group>"; };
D0C7D41E24F76169001EBDBB /* Metatext.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Metatext.entitlements; sourceTree = "<group>"; };
D0C7D41F24F76169001EBDBB /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -132,36 +105,23 @@
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationTypesPreferencesView.swift; sourceTree = "<group>"; };
D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabNavigationView.swift; sourceTree = "<group>"; };
D0C7D43124F76169001EBDBB /* StatusListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusListViewController.swift; sourceTree = "<group>"; };
D0C7D45024F76169001EBDBB /* AlertItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertItem.swift; sourceTree = "<group>"; };
D0C7D45224F76169001EBDBB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
D0C7D45424F76169001EBDBB /* MetatextApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = "<group>"; };
D0C7D45524F76169001EBDBB /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
D0C7D45724F76169001EBDBB /* Localizable.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
D0C7D45824F76169001EBDBB /* Localizable.stringsdict */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = "<group>"; };
D0C7D45A24F76169001EBDBB /* PostingReadingPreferencesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesViewModel.swift; sourceTree = "<group>"; };
D0C7D45B24F76169001EBDBB /* SecondaryNavigationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryNavigationViewModel.swift; sourceTree = "<group>"; };
D0C7D45C24F76169001EBDBB /* TabNavigationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabNavigationViewModel.swift; sourceTree = "<group>"; };
D0C7D45D24F76169001EBDBB /* NotificationTypesPreferencesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationTypesPreferencesViewModel.swift; sourceTree = "<group>"; };
D0C7D45E24F76169001EBDBB /* RootViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = "<group>"; };
D0C7D45F24F76169001EBDBB /* IdentitiesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentitiesViewModel.swift; sourceTree = "<group>"; };
D0C7D46024F76169001EBDBB /* AddIdentityViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModel.swift; sourceTree = "<group>"; };
D0C7D46124F76169001EBDBB /* PreferencesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesViewModel.swift; sourceTree = "<group>"; };
D0C7D46224F76169001EBDBB /* StatusViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusViewModel.swift; sourceTree = "<group>"; };
D0C7D46324F76169001EBDBB /* StatusListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusListViewModel.swift; sourceTree = "<group>"; };
D0C7D46A24F76169001EBDBB /* String+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMutableAttributedString+Extensions.swift"; sourceTree = "<group>"; };
D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
D0C7D46D24F76169001EBDBB /* Publisher+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = "<group>"; };
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KingfisherOptionsInfo+Extensions.swift"; sourceTree = "<group>"; };
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = "<group>"; };
D0C7D47024F76169001EBDBB /* Date+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
D0C7D47124F76169001EBDBB /* Data+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = "<group>"; };
D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = "<group>"; };
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 = "<group>"; };
D0E5361D24E3EB4D00FB1CE1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D0E5362824E4A06B00FB1CE1 /* Notification Service Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Notification Service Extension.entitlements"; sourceTree = "<group>"; };
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddIdentityViewModelTests.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@ -250,7 +207,6 @@
isa = PBXGroup;
children = (
D0666A2524C677B400F3F04B /* Info.plist */,
D0ED1B6C24CE0EED00B4899C /* View Models */,
);
path = Tests;
sourceTree = "<group>";
@ -302,14 +258,6 @@
path = "View Controllers";
sourceTree = "<group>";
};
D0C7D43824F76169001EBDBB /* Model */ = {
isa = PBXGroup;
children = (
D0C7D45024F76169001EBDBB /* AlertItem.swift */,
);
path = Model;
sourceTree = "<group>";
};
D0C7D45324F76169001EBDBB /* System */ = {
isa = PBXGroup;
children = (
@ -328,35 +276,12 @@
path = Localizations;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
D0ED1B6C24CE0EED00B4899C /* View Models */ = {
isa = PBXGroup;
children = (
D0ED1B6D24CE100C00B4899C /* AddIdentityViewModelTests.swift */,
D052BBC624D749C800A80A7A /* RootViewModelTests.swift */,
);
path = "View Models";
sourceTree = "<group>";
};
D0ED1BB224CE3A1600B4899C /* Preview */ = {
isa = PBXGroup;
children = (
D04FD74124D4AA34007D572D /* PreviewMocks.swift */,
);
path = Preview;
sourceTree = "<group>";
};
/* 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 */
};

View File

@ -12,7 +12,7 @@
},
{
"package": "CombineExpectations",
"repositoryURL": "https://github.com/groue/CombineExpectations",
"repositoryURL": "https://github.com/groue/CombineExpectations.git",
"state": {
"branch": null,
"revision": "96d5604151c94b21fbca6877b237e80af9e821dd",

View File

@ -1,8 +0,0 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
struct AlertItem: Identifiable {
let id = UUID()
let error: Error
}

View File

@ -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<AnyCancellable>()
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

View File

@ -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()

View File

@ -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")

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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<DelegateEvent, Never>
public init(
getNotificationSettings: @escaping (@escaping (UNNotificationSettings) -> Void) -> Void,
requestAuthorization: @escaping (UNAuthorizationOptions, @escaping (Bool, Error?) -> Void) -> Void,
delegateEvents: AnyPublisher<DelegateEvent, Never>) {
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<DelegateEvent, Never>
init(subject: PassthroughSubject<DelegateEvent, Never>) {
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<DelegateEvent, Never>()
var delegate: Delegate? = Delegate(subject: subject)
userNotificationCenter.delegate = delegate
return UserNotificationClient(
getNotificationSettings: userNotificationCenter.getNotificationSettings,
requestAuthorization: userNotificationCenter.requestAuthorization,
delegateEvents: subject
.handleEvents(receiveCancel: { delegate = nil })
.eraseToAnyPublisher())
}
}

View File

@ -29,7 +29,10 @@ extension WebAuthSession {
}
webAuthSession.presentationContextProvider = presentationContextProvider
webAuthSession.start()
DispatchQueue.main.async {
webAuthSession.start()
}
}
.eraseToAnyPublisher()
}

View File

@ -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<Identity, Never> in
self?.observationErrorsInput.send(error)

View File

@ -4,15 +4,14 @@ import Foundation
import Combine
import UserNotifications
public class UserNotificationService: NSObject {
private let userNotificationCenter: UNUserNotificationCenter
public struct UserNotificationService {
let events: AnyPublisher<UserNotificationClient.DelegateEvent, Never>
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<Bool, Error> {
getNotificationSettings()
.map(\.authorizationStatus)
.flatMap { [weak self] status -> AnyPublisher<Bool, Error> in
.flatMap { status -> AnyPublisher<Bool, Error> 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<UNNotificationSettings, Never> {
Future<UNNotificationSettings, Never> { [weak self] promise in
self?.userNotificationCenter.getNotificationSettings { promise(.success($0)) }
Future<UNNotificationSettings, Never> { promise in
userNotificationClient.getNotificationSettings { promise(.success($0)) }
}
.eraseToAnyPublisher()
}
func requestProvisionalAuthorization() -> AnyPublisher<Bool, Error> {
Future<Bool, Error> { [weak self] promise in
self?.userNotificationCenter.requestAuthorization(
options: [.alert, .sound, .badge, .provisional]) { granted, error in
Future<Bool, Error> { 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)
}
}

View File

@ -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)
}
}

View File

@ -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())
}

View File

@ -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)

View File

@ -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
}
}
}

View File

@ -2,6 +2,7 @@
import SwiftUI
import Combine
import ViewModels
class StatusListViewController: UITableViewController {
private let viewModel: StatusListViewModel

5
ViewModels/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/

34
ViewModels/Package.swift Normal file
View File

@ -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"])
]
)

View File

@ -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)
}
}

View File

@ -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<UUID, Never>
public class AddIdentityViewModel: ObservableObject {
@Published public var urlFieldText = ""
@Published public var alertItem: AlertItem?
@Published public private(set) var loading = false
public let addedIdentityID: AnyPublisher<UUID, Never>
private let allIdentitiesService: AllIdentitiesService
private let addedIdentityIDInput = PassthroughSubject<UUID, Never>()
@ -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 })

View File

@ -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,

View File

@ -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<Void, Never>
public class EditFilterViewModel: ObservableObject {
@Published public var filter: Filter
@Published public var saving = false
@Published public var alertItem: AlertItem?
public let saveCompleted: AnyPublisher<Void, Never>
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 }

View File

@ -0,0 +1,8 @@
// Copyright © 2020 Metabolist. All rights reserved.
import Foundation
public struct AlertItem: Identifiable {
public let id = UUID()
public let error: Error
}

View File

@ -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
}
}

View File

@ -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<AnyCancellable>()
@ -28,7 +28,7 @@ class FiltersViewModel: ObservableObject {
}
}
extension FiltersViewModel {
public extension FiltersViewModel {
func refreshFilters() {
identityService.refreshFilters()
.assignErrorsToAlertItem(to: \.alertItem, on: self)

View File

@ -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<AnyCancellable>()

View File

@ -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<AnyCancellable>()
@ -29,7 +29,7 @@ class ListsViewModel: ObservableObject {
}
}
extension ListsViewModel {
public extension ListsViewModel {
func refreshLists() {
identityService.refreshLists()
.assignErrorsToAlertItem(to: \.alertItem, on: self)

View File

@ -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<AnyCancellable>()

View File

@ -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<AnyCancellable>()

View File

@ -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)
}

View File

@ -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<String, Error>
private var cancellables = Set<AnyCancellable>()
init(appDelegate: AppDelegate,
allIdentitiesService: AllIdentitiesService,
userNotificationService: UserNotificationService) {
self.appDelegate = appDelegate
self.allIdentitiesService = allIdentitiesService
self.userNotificationService = userNotificationService
public init(environment: AppEnvironment,
registerForRemoteNotifications: @escaping () -> AnyPublisher<String, Error>) 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:))

View File

@ -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)
}

View File

@ -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 },

View File

@ -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<AnyPublisher<Never, Error>, 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<AnyPublisher<Never, Error>, Never>
private let statusService: StatusService
private let eventsInput = PassthroughSubject<AnyPublisher<Never, Error>, Never>()
@ -49,7 +49,7 @@ struct StatusViewModel {
}
}
extension StatusViewModel {
public extension StatusViewModel {
var shouldDisplaySensitiveContent: Bool {
if statusService.status.displayStatus.sensitive {
return sensitiveContentToggled

View File

@ -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<AnyCancellable>()
@ -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 }
}

View File

@ -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() {
}
}

View File

@ -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<AnyCancellable>()
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))

View File

@ -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

View File

@ -2,6 +2,7 @@
import UIKit
import Kingfisher
import ViewModels
class AttachmentView: UIView {
let imageView = AnimatedImageView()

View File

@ -1,6 +1,7 @@
// Copyright © 2020 Metabolist. All rights reserved.
import UIKit
import ViewModels
class AttachmentsView: UIView {
private let containerStackView = UIStackView()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -3,6 +3,7 @@
import AVKit
import Kingfisher
import UIKit
import ViewModels
protocol StatusTableViewCellDelegate: class {
func statusTableViewCellDidHaveShareButtonTapped(_ cell: StatusTableViewCell)

View File

@ -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

View File

@ -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