diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 1a0fb9f6d..46879c2e7 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -146,6 +146,7 @@ 9F5C2A8B257E6C4900DE9B4B /* ImapError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5C2A8A257E6C4900DE9B4B /* ImapError.swift */; }; 9F5C2A92257E94DF00DE9B4B /* Imap+MessageOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5C2A91257E94DF00DE9B4B /* Imap+MessageOperations.swift */; }; 9F5C2A99257E94E900DE9B4B /* Gmail+MessageOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5C2A98257E94E900DE9B4B /* Gmail+MessageOperations.swift */; }; + 9F5EF7012753DA7000CC3F88 /* UI Guidance.md in Resources */ = {isa = PBXBuildFile; fileRef = 9F5EF7002753DA7000CC3F88 /* UI Guidance.md */; }; 9F5F501D26F90AE100294FA2 /* OrganisationalRulesServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5F501C26F90AE100294FA2 /* OrganisationalRulesServiceMock.swift */; }; 9F5F503526F90E5F00294FA2 /* ClientConfigurationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5F503426F90E5F00294FA2 /* ClientConfigurationServiceTests.swift */; }; 9F5F503C26FA6C5E00294FA2 /* CurrentUserEmailMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5F503B26FA6C5E00294FA2 /* CurrentUserEmailMock.swift */; }; @@ -582,6 +583,7 @@ 9F5C2A8A257E6C4900DE9B4B /* ImapError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImapError.swift; sourceTree = ""; }; 9F5C2A91257E94DF00DE9B4B /* Imap+MessageOperations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Imap+MessageOperations.swift"; sourceTree = ""; }; 9F5C2A98257E94E900DE9B4B /* Gmail+MessageOperations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Gmail+MessageOperations.swift"; sourceTree = ""; }; + 9F5EF7002753DA7000CC3F88 /* UI Guidance.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "UI Guidance.md"; sourceTree = ""; }; 9F5F501C26F90AE100294FA2 /* OrganisationalRulesServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganisationalRulesServiceMock.swift; sourceTree = ""; }; 9F5F503426F90E5F00294FA2 /* ClientConfigurationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientConfigurationServiceTests.swift; sourceTree = ""; }; 9F5F503B26FA6C5E00294FA2 /* CurrentUserEmailMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentUserEmailMock.swift; sourceTree = ""; }; @@ -1636,6 +1638,7 @@ 51E1673C270DAFF900D27C52 /* Localizable.stringsdict */, 9F71630A234FC73E0031645E /* Localizable.strings */, D269E025240EAD9A000495C3 /* Naming Convention.md */, + 9F5EF7002753DA7000CC3F88 /* UI Guidance.md */, ); path = Resources; sourceTree = ""; @@ -2296,6 +2299,7 @@ C132B9BE1EC2DBD800763715 /* LaunchScreen.storyboard in Resources */, 51E1673D270DAFF900D27C52 /* Localizable.stringsdict in Resources */, C132B9BB1EC2DBD800763715 /* Assets.xcassets in Resources */, + 9F5EF7012753DA7000CC3F88 /* UI Guidance.md in Resources */, 9F716308234FC73E0031645E /* Localizable.strings in Resources */, D2F6D13E2434FF1400DB4065 /* providers_custom.json in Resources */, 32DCA6A3C9D59DD16B136772 /* flowcrypt-ios-prod.js.txt in Resources */, diff --git a/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift b/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift index d33aeeee9..e18429f98 100644 --- a/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift +++ b/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift @@ -6,9 +6,17 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // -import UIKit +import FlowCryptUI +import AsyncDisplayKit + +final class InvalidStorageViewController: TableNodeViewController { + private enum Parts: Int, CaseIterable { + case screenTitle + case title + case description + case button + } -final class InvalidStorageViewController: UIViewController { private let error: Error private let encryptedStorage: EncryptedStorageType private let router: GlobalRouterType @@ -17,7 +25,7 @@ final class InvalidStorageViewController: UIViewController { self.error = error self.encryptedStorage = encryptedStorage self.router = router - super.init(nibName: nil, bundle: nil) + super.init(node: TableNode()) } @available(*, unavailable) @@ -27,53 +35,12 @@ final class InvalidStorageViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - title = "invalid_storage_title".localized - view.backgroundColor = .backgroundColor - let font = UIFont.systemFont(ofSize: 16) - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = "invalid_storage_text".localized - label.numberOfLines = 0 - label.font = font - label.textColor = .mainTextColor - view.addSubview(label) - - let textView = UITextView() - textView.translatesAutoresizingMaskIntoConstraints = false - textView.isEditable = false - textView.text = error.localizedDescription - textView.font = font - textView.textColor = .mainTextColor - textView.backgroundColor = .backgroundColor - view.addSubview(textView) - - let button = UIButton(type: .custom) - button.translatesAutoresizingMaskIntoConstraints = false - button.backgroundColor = .red - button.setTitle("invalid_storage_reset_button".localized, for: .normal) - button.setTitleColor(.white, for: .normal) - button.addTarget(self, action: #selector(handleTap), for: .touchUpInside) - button.titleLabel?.font = UIFont.systemFont(ofSize: 16, weight: .bold) - button.layer.cornerRadius = 5 - view.addSubview(button) - - NSLayoutConstraint.activate([ - label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), - label.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16), - label.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16), - - textView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 10), - textView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16), - textView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16), - textView.bottomAnchor.constraint(equalTo: button.topAnchor, constant: -10), - - button.heightAnchor.constraint(equalToConstant: 50), - button.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16), - button.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16), - button.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -8) - ]) + node.delegate = self + node.dataSource = self + node.bounces = false + node.reloadData() } @objc private func handleTap() { @@ -85,3 +52,73 @@ final class InvalidStorageViewController: UIViewController { } } } + +extension InvalidStorageViewController: ASTableDelegate, ASTableDataSource { + func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { + Parts.allCases.count + } + + func tableNode(_ node: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { + return { [weak self] in + guard let self = self, let part = Parts(rawValue: indexPath.row) else { + return ASCellNode() + } + + let insets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) + switch part { + case .screenTitle: + return SetupTitleNode( + SetupTitleNode.Input( + title: "invalid_storage_title".localized + .attributed( + .bold(18), + color: .mainTextColor, + alignment: .center + ), + insets: insets, + backgroundColor: .backgroundColor + ) + ) + case .title: + return SetupTitleNode( + SetupTitleNode.Input( + title: "invalid_storage_text".localized + .attributed( + .regular(16), + color: .mainTextColor, + alignment: .center + ), + insets: insets, + backgroundColor: .backgroundColor + ) + ) + case .description: + return SetupTitleNode( + SetupTitleNode.Input( + title: self.error.localizedDescription.attributed( + .regular(16), + color: .mainTextColor, + alignment: .center + ), + insets: insets, + backgroundColor: .backgroundColor + ) + ) + case .button: + return ButtonCellNode( + input: ButtonCellNode.Input( + title: "invalid_storage_reset_button".localized.attributed( + .bold(16), + color: .white, + alignment: .center + ), + insets: insets, + color: .red + ) + ) { [weak self] in + self?.handleTap() + } + } + } + } +} diff --git a/FlowCrypt/Functionality/Services/GlobalRouter.swift b/FlowCrypt/Functionality/Services/GlobalRouter.swift index 2ffed99ea..1e7b250f4 100644 --- a/FlowCrypt/Functionality/Services/GlobalRouter.swift +++ b/FlowCrypt/Functionality/Services/GlobalRouter.swift @@ -123,6 +123,7 @@ extension GlobalRouter: GlobalRouterType { proceed(with: session) } + @MainActor private func validateEncryptedStorage(_ completion: () -> Void) { let storage = EncryptedStorage() do { diff --git a/FlowCrypt/Resources/UI Guidance.md b/FlowCrypt/Resources/UI Guidance.md new file mode 100644 index 000000000..373d5b791 --- /dev/null +++ b/FlowCrypt/Resources/UI Guidance.md @@ -0,0 +1,21 @@ +## FlowCrypt UI Guidance + + Instead of using UIKit views and layout them we can use ASTableNode (similar to UITableView). + and build or UI in more flexible and reusable way by layouting independent ASTableNodeCells (UITableViewCell) + They will not dependent on already existed elements. So for example we can add or remove parts without touching any other elements + ASDK (Texture) can calculate UI not on Main Thread. For this we can use ASCellNodeBlock with ASCellNodes as return element + + For existed nodes please check FlowCryptUI. This contains all elements which are used it the app. + Please use elements from FlowCryptUI or add new one. + Consider this as design system for the app. + + Each node built in declarative way and can be build with Input (consider as view model for node) + + For button - ButtonCellNode can be used with appropriate ButtonCellNode.Input + For titles - SetupTitleNode with input + + Usually all inputs for node can be build with attributed text, insets and action if supported + There are a lot of usefull extensions for attributed text which uses common styling in app + (FlowCryptCommon -> String extensions) + + Also app use Decorators for each View Controller focused on ui styling.