From ce23339a408bd658765fb8716027185011f18bba Mon Sep 17 00:00:00 2001 From: svojsu Date: Tue, 29 Oct 2024 14:57:30 +0200 Subject: [PATCH 01/12] new QR code and layout --- Podfile | 1 + Podfile.lock | 14 ++++- novawallet.xcodeproj/project.pbxproj | 4 ++ .../QRCreation/BarcodeCreationError.swift | 5 ++ .../QRCreation/QRCreationOperation.swift | 62 +++++++++++-------- .../AssetReceive/AssetReceivePresenter.swift | 13 ++++ .../AssetReceive/AssetReceiveProtocols.swift | 1 + .../AssetReceiveViewController.swift | 39 +++++++++--- .../AssetReceiveViewFactory.swift | 3 + .../AssetReceive/AssetReceiveViewLayout.swift | 35 +++++++---- .../QRCreationOperationFactory.swift | 22 ++++++- novawallet/en.lproj/Localizable.strings | 1 + 12 files changed, 149 insertions(+), 51 deletions(-) create mode 100644 novawallet/Common/QRCreation/BarcodeCreationError.swift diff --git a/Podfile b/Podfile index 805c96ad77..02e1f1206c 100644 --- a/Podfile +++ b/Podfile @@ -4,6 +4,7 @@ platform :ios, '14.0' abstract_target 'novawalletAll' do use_frameworks! + pod 'DSF_QRCode', '~> 18.0.0' pod 'SubstrateSdk', :git => 'https://github.com/nova-wallet/substrate-sdk-ios.git', :tag => '3.2.2' pod 'SwiftLint' pod 'R.swift', :inhibit_warnings => true diff --git a/Podfile.lock b/Podfile.lock index b5c0ec8df8..78be17d368 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -646,6 +646,9 @@ PODS: - Cuckoo (1.7.1): - Cuckoo/Swift (= 1.7.1) - Cuckoo/Swift (1.7.1) + - DSF_QRCode (18.0.0): + - SwiftImageReadWrite (~> 1.6.1) + - SwiftQRCodeGenerator (~> 2.0.2) - EthereumSignTypedDataUtil (0.1.2): - BigInt (~> 5.0) - CryptoSwift (~> 1.4) @@ -898,7 +901,9 @@ PODS: - CocoaLumberjack (~> 3.0) - SwiftAlgorithms (1.0.0) - SwiftFormat/CLI (0.47.13) + - SwiftImageReadWrite (1.6.1) - SwiftLint (0.43.1) + - SwiftQRCodeGenerator (2.0.2) - SwiftRLP (1.1): - BigInt (~> 5.0) - SwiftyBeaver (1.9.3) @@ -963,6 +968,7 @@ PODS: DEPENDENCIES: - CDMarkdownKit (from `https://github.com/nova-wallet/CDMarkdownKit.git`, tag `2.5.2`) - Cuckoo + - DSF_QRCode (~> 18.0.0) - EthereumSignTypedDataUtil (from `https://github.com/ERussel/EthereumSignTypedDataUtil.git`, tag `0.1.3`) - FirebaseAppCheck - FirebaseAuth @@ -1001,6 +1007,7 @@ SPEC REPOS: - CocoaLumberjack - CryptoSwift - Cuckoo + - DSF_QRCode - FirebaseAppCheck - FirebaseAppCheckInterop - FirebaseAuth @@ -1035,7 +1042,9 @@ SPEC REPOS: - Sourcery - SwiftAlgorithms - SwiftFormat + - SwiftImageReadWrite - SwiftLint + - SwiftQRCodeGenerator - SwiftyBeaver - TweetNacl - Web3Core @@ -1133,6 +1142,7 @@ SPEC CHECKSUMS: CocoaLumberjack: b7e05132ff94f6ae4dfa9d5bce9141893a21d9da CryptoSwift: c4f2debceb38bf44c80659afe009f71e23e4a082 Cuckoo: 9e258d68137c411df47c6390f72901d5276b4f03 + DSF_QRCode: 3fe0acc968ce8588a9b1dabb9557a364472f5aaf EthereumSignTypedDataUtil: ae4e33b21e51ee046a86a65b02843090b8bbd3f9 FirebaseAppCheck: 4bb8047366c2c975583c9eff94235f8f2c5b342d FirebaseAppCheckInterop: e81bdb1cdb82f8e0cef353ba5018a8402682032c @@ -1176,7 +1186,9 @@ SPEC CHECKSUMS: SVGKit: 132b010efbf57ec345309fe4a7f627c0a40c5d63 SwiftAlgorithms: 38dda4731d19027fdeee1125f973111bf3386b53 SwiftFormat: 73573b89257437c550b03d934889725fbf8f75e5 + SwiftImageReadWrite: 69f6521a74fdddbb61b2cc844150ec64de2cb4c7 SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52 + SwiftQRCodeGenerator: cc02fed209335064d0b0dd61b2a0874b9bc6bd5a SwiftRLP: f58417bfceecd45394fc619ccad14cf16e4ae6c1 SwiftyBeaver: 2e8acd6fc90c6d0a27055867a290794926d57c02 TweetNacl: 3abf4d1d2082b0114e7a67410e300892448951e6 @@ -1187,6 +1199,6 @@ SPEC CHECKSUMS: ZMarkupParser: a92d31ba40695b790f1da5fec98c3d4505341aff ZNSTextAttachment: 1ddd53660a8d3c42dbb716bf6866ffce22c44181 -PODFILE CHECKSUM: 73c4593a21ffac681c225d143f0787a49f9e9826 +PODFILE CHECKSUM: 4ff91aa8a820a3a2179b0cc901ed9c7aa46ce619 COCOAPODS: 1.15.2 diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 4e514352c4..4f53d88e8e 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -958,6 +958,7 @@ 2D389B172C875B3200256C7B /* SwipeGovInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D389B162C875B3200256C7B /* SwipeGovInteractor.swift */; }; 2D442CEF2CD0F7DF00A0380F /* AppearanceDepending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D442CEE2CD0F7DF00A0380F /* AppearanceDepending.swift */; }; 2D442CF12CD0F85400A0380F /* IconAppearanceDepending.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D442CF02CD0F85400A0380F /* IconAppearanceDepending.swift */; }; + 2D442CF32CD103ED00A0380F /* BarcodeCreationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D442CF22CD103ED00A0380F /* BarcodeCreationError.swift */; }; 2D4F55012CCA60E900B65B76 /* AssetsSearchCollectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4F55002CCA60E900B65B76 /* AssetsSearchCollectionManager.swift */; }; 2D4F55032CCA60F300B65B76 /* AssetsSearchCollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4F55022CCA60F300B65B76 /* AssetsSearchCollectionViewDataSource.swift */; }; 2D4F55052CCA611200B65B76 /* AssetsSearchCollectionViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4F55042CCA611200B65B76 /* AssetsSearchCollectionViewDelegate.swift */; }; @@ -6137,6 +6138,7 @@ 2D389B162C875B3200256C7B /* SwipeGovInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeGovInteractor.swift; sourceTree = ""; }; 2D442CEE2CD0F7DF00A0380F /* AppearanceDepending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceDepending.swift; sourceTree = ""; }; 2D442CF02CD0F85400A0380F /* IconAppearanceDepending.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconAppearanceDepending.swift; sourceTree = ""; }; + 2D442CF22CD103ED00A0380F /* BarcodeCreationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarcodeCreationError.swift; sourceTree = ""; }; 2D4F55002CCA60E900B65B76 /* AssetsSearchCollectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsSearchCollectionManager.swift; sourceTree = ""; }; 2D4F55022CCA60F300B65B76 /* AssetsSearchCollectionViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsSearchCollectionViewDataSource.swift; sourceTree = ""; }; 2D4F55042CCA611200B65B76 /* AssetsSearchCollectionViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetsSearchCollectionViewDelegate.swift; sourceTree = ""; }; @@ -19789,6 +19791,7 @@ children = ( 84BC703E289DBE6C008A9758 /* QRCreationOperation.swift */, 84BC7040289DBF62008A9758 /* QRDisplayView.swift */, + 2D442CF22CD103ED00A0380F /* BarcodeCreationError.swift */, ); path = QRCreation; sourceTree = ""; @@ -28455,6 +28458,7 @@ 846DA55B2A20A56D006CD6C1 /* OffchainMultistakingUpdateService.swift in Sources */, 0C3205CA2A895489002EB914 /* EvmGasLimitProvider.swift in Sources */, 7764E2772BA43DA1002F54AA /* SettingsSectionFooterView.swift in Sources */, + 2D442CF32CD103ED00A0380F /* BarcodeCreationError.swift in Sources */, AE180C8B30831C9BAA39763A /* ParaStkYieldBoostSetupViewController.swift in Sources */, 8824D4222902D92F0022D778 /* ReferendumFullDetailsInteractor.swift in Sources */, 88B1862A28EF30A600D49854 /* YourVoteView.swift in Sources */, diff --git a/novawallet/Common/QRCreation/BarcodeCreationError.swift b/novawallet/Common/QRCreation/BarcodeCreationError.swift new file mode 100644 index 0000000000..b73fc80a32 --- /dev/null +++ b/novawallet/Common/QRCreation/BarcodeCreationError.swift @@ -0,0 +1,5 @@ +enum BarcodeCreationError: Error { + case generatorUnavailable + case generatedImageInvalid + case bitmapImageCreationFailed +} diff --git a/novawallet/Common/QRCreation/QRCreationOperation.swift b/novawallet/Common/QRCreation/QRCreationOperation.swift index 4dca962da0..6aabc1829c 100644 --- a/novawallet/Common/QRCreation/QRCreationOperation.swift +++ b/novawallet/Common/QRCreation/QRCreationOperation.swift @@ -1,6 +1,7 @@ import Foundation import CoreImage import Operation_iOS +import QRCode enum QRCreationOperationError: Error { case generatorUnavailable @@ -11,49 +12,60 @@ enum QRCreationOperationError: Error { final class QRCreationOperation: BaseOperation { let payloadClosure: () throws -> Data let qrSize: CGSize + let logoSize: CGSize? - init(payload: Data, qrSize: CGSize) { + init( + payload: Data, + qrSize: CGSize, + logoSize: CGSize? = nil + ) { payloadClosure = { payload } self.qrSize = qrSize + self.logoSize = logoSize super.init() } - init(qrSize: CGSize, payloadClosure: @escaping () throws -> Data) { + init( + qrSize: CGSize, + logoSize: CGSize?, + payloadClosure: @escaping () throws -> Data + ) { self.qrSize = qrSize + self.logoSize = logoSize self.payloadClosure = payloadClosure } override func performAsync(_ callback: @escaping (Result) -> Void) throws { - guard let filter = CIFilter(name: "CIQRCodeGenerator") else { - throw QRCreationOperationError.generatorUnavailable - } - - let payload = try payloadClosure() - - filter.setValue(payload, forKey: "inputMessage") - filter.setValue("M", forKey: "inputCorrectionLevel") + let data = try payloadClosure() + let qrDoc = QRCode.Document(data: data) + qrDoc.design.backgroundColor(UIColor.white.cgColor) + qrDoc.design.shape.eye = QRCode.EyeShape.Squircle() + qrDoc.design.shape.onPixels = QRCode.PixelShape.Circle(insetFraction: 0.2) + qrDoc.design.style.onPixels = QRCode.FillStyle.Solid(UIColor.black.cgColor) + qrDoc.design.shape.offPixels = nil + qrDoc.design.style.offPixels = nil - guard let qrImage = filter.outputImage else { - throw QRCreationOperationError.generatedImageInvalid - } + if let logoSize { + let scaledSize = CGSize( + width: logoSize.width * UIScreen.main.scale, + height: logoSize.height * UIScreen.main.scale + ) - let transformedImage: CIImage + guard let image = UIImage.background(from: .white, size: scaledSize)?.cgImage else { + return + } - if qrImage.extent.size.width * qrImage.extent.height > 0.0 { - let transform = CGAffineTransform( - scaleX: qrSize.width / qrImage.extent.width, - y: qrSize.height / qrImage.extent.height - ) - transformedImage = qrImage.transformed(by: transform) - } else { - transformedImage = qrImage + qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: image) } - let context = CIContext() + let scaledSize = CGSize( + width: qrSize.width * UIScreen.main.scale, + height: qrSize.height * UIScreen.main.scale + ) - guard let cgImage = context.createCGImage(transformedImage, from: transformedImage.extent) else { - throw QRCreationOperationError.bitmapImageCreationFailed + guard let cgImage = qrDoc.cgImage(scaledSize) else { + throw BarcodeCreationError.bitmapImageCreationFailed } callback(.success(UIImage(cgImage: cgImage))) diff --git a/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift b/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift index 362e384920..8ed1819266 100644 --- a/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift +++ b/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift @@ -9,6 +9,7 @@ final class AssetReceivePresenter { let interactor: AssetReceiveInteractorInputProtocol let iconGenerator: IconGenerating let accountShareFactory: NovaAccountShareFactoryProtocol + let networkViewModelFactory: NetworkViewModelFactoryProtocol let localizationManager: LocalizationManagerProtocol let logger: LoggerProtocol? @@ -23,6 +24,7 @@ final class AssetReceivePresenter { wireframe: AssetReceiveWireframeProtocol, iconGenerator: IconGenerating, accountShareFactory: NovaAccountShareFactoryProtocol, + networkViewModelFactory: NetworkViewModelFactoryProtocol, localizationManager: LocalizationManagerProtocol, logger: LoggerProtocol? ) { @@ -31,6 +33,7 @@ final class AssetReceivePresenter { self.iconGenerator = iconGenerator self.accountShareFactory = accountShareFactory self.logger = logger + self.networkViewModelFactory = networkViewModelFactory self.localizationManager = localizationManager } @@ -48,6 +51,14 @@ final class AssetReceivePresenter { ) return viewModel } + + private func provideNetwork() { + guard let chain else { return } + + let networkViewModel = networkViewModelFactory.createViewModel(from: chain) + + view?.didReceive(networkViewModel: networkViewModel) + } } extension AssetReceivePresenter: AssetReceivePresenterProtocol { @@ -101,6 +112,8 @@ extension AssetReceivePresenter: AssetReceiveInteractorOutputProtocol { self.account = account self.chain = chain + provideNetwork() + let chainAccountViewModel = createChainAccountViewModel( for: account.chainAccount.accountId, chain: chain diff --git a/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift b/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift index 837df72928..83c505264a 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift @@ -1,6 +1,7 @@ import UIKit protocol AssetReceiveViewProtocol: ControllerBackedProtocol { + func didReceive(networkViewModel: NetworkViewModel) func didReceive(chainAccountViewModel: ChainAccountViewModel, token: String) func didReceive(qrImage: UIImage) } diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift index 758595b745..83b503db01 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift @@ -6,7 +6,6 @@ final class AssetReceiveViewController: UIViewController, ViewHolder { let presenter: AssetReceivePresenterProtocol private var cachedBoundsWidth: CGFloat? - private var token: String = "" init( presenter: AssetReceivePresenterProtocol, @@ -46,12 +45,9 @@ final class AssetReceiveViewController: UIViewController, ViewHolder { } private func setupLocalization() { - let languages = selectedLocale.rLanguages - rootView.titleLabel.text = R.string.localizable.walletReceiveDescription(preferredLanguages: languages) rootView.shareButton.imageWithTitleView?.title = R.string.localizable.walletReceiveShareTitle( - preferredLanguages: languages + preferredLanguages: selectedLocale.rLanguages ) - update(token: token) } private func setupHandlers() { @@ -59,14 +55,28 @@ final class AssetReceiveViewController: UIViewController, ViewHolder { rootView.accountDetailsView.addTarget(self, action: #selector(didTapOnAccount), for: .touchUpInside) } - private func update(token: String) { - self.token = token - navigationItem.title = R.string.localizable.walletReceiveTitleFormat( + private func updateTitleDetails( + chainName: String, + token: String + ) { + let languages = selectedLocale.rLanguages + + rootView.titleLabel.text = R.string.localizable.walletReceiveTitleFormat( token, - preferredLanguages: selectedLocale.rLanguages + preferredLanguages: languages + ) + + rootView.detailsLabel.text = R.string.localizable.walletReceiveDetailsFormat( + token, + chainName, + preferredLanguages: languages ) } + private func updateNavigationBar() { + navigationItem.titleView = rootView.chainView + } + @objc private func didTapShare() { presenter.share() } @@ -79,12 +89,21 @@ final class AssetReceiveViewController: UIViewController, ViewHolder { extension AssetReceiveViewController: AssetReceiveViewProtocol { func didReceive(chainAccountViewModel: ChainAccountViewModel, token: String) { rootView.accountDetailsView.chainAccountView.bind(viewModel: chainAccountViewModel) - update(token: token) + + updateTitleDetails( + chainName: chainAccountViewModel.networkName, + token: token + ) } func didReceive(qrImage: UIImage) { rootView.qrView.imageView.image = qrImage } + + func didReceive(networkViewModel: NetworkViewModel) { + rootView.chainView.bind(viewModel: networkViewModel) + updateNavigationBar() + } } extension AssetReceiveViewController: Localizable { diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift index fb2a4ba912..84c042074d 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift @@ -28,11 +28,14 @@ struct AssetReceiveViewFactory { localizationManager: localizationManager ) + let networkViewModelFactory = NetworkViewModelFactory() + let presenter = AssetReceivePresenter( interactor: interactor, wireframe: wireframe, iconGenerator: PolkadotIconGenerator(), accountShareFactory: accountShareFactory, + networkViewModelFactory: networkViewModelFactory, localizationManager: localizationManager, logger: Logger.shared ) diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift index c60ab8ff76..575f2f6619 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift @@ -10,11 +10,23 @@ final class AssetReceiveViewLayout: UIView { return view }() + let chainView = AssetListChainView() + let accountDetailsView: ChainAccountControl = .create { $0.chainAccountView.actionIconView.image = R.image.iconMore()?.tinted(with: R.color.colorIconSecondary()!) } - let titleLabel = UILabel(style: .semiboldBodyPrimary, textAlignment: .center) + let titleLabel: UILabel = .create { view in + view.apply(style: .title3Primary) + view.textAlignment = .center + } + + let detailsLabel: UILabel = .create { view in + view.apply(style: .footnoteSecondary) + view.numberOfLines = 0 + view.textAlignment = .center + } + let qrView: QRDisplayView = .create { $0.contentInsets = Constants.qrViewContentInsets } let shareButton: TriangularedButton = .create { $0.applyDefaultStyle() } @@ -36,15 +48,10 @@ final class AssetReceiveViewLayout: UIView { $0.top.equalTo(safeAreaLayoutGuide.snp.top) $0.leading.trailing.bottom.equalToSuperview() } - containerView.stackView.addArrangedSubview(accountDetailsView) containerView.stackView.addArrangedSubview(titleLabel) + containerView.stackView.addArrangedSubview(detailsLabel) containerView.stackView.addArrangedSubview(qrView) - accountDetailsView.snp.makeConstraints { - $0.height.equalTo(Constants.accountDetailsViewHeight) - $0.width.equalToSuperview().inset(Constants.containerHorizontalOffset) - } - qrView.snp.makeConstraints { $0.width.equalToSuperview().multipliedBy(Constants.qrViewSizeRatio) $0.width.equalTo(Constants.qrViewPlaceHolderWidth).priority(.high) @@ -52,10 +59,13 @@ final class AssetReceiveViewLayout: UIView { } containerView.stackView.setCustomSpacing( - Constants.accountDetailsTitleVerticalSpace, - after: accountDetailsView + Constants.titleDetailsVerticalSpace, + after: titleLabel + ) + containerView.stackView.setCustomSpacing( + Constants.detailsQRVerticalSpace, + after: detailsLabel ) - containerView.stackView.setCustomSpacing(Constants.titleQRVerticalSpace, after: titleLabel) addSubview(shareButton) shareButton.snp.makeConstraints { @@ -75,13 +85,14 @@ extension AssetReceiveViewLayout { static let qrCodeMinimumWidth: CGFloat = 120 static let accountDetailsViewHeight: CGFloat = 52 static let accountDetailsTitleVerticalSpace: CGFloat = 52 - static let titleQRVerticalSpace: CGFloat = 36 + static let detailsQRVerticalSpace: CGFloat = 36 + static let titleDetailsVerticalSpace: CGFloat = 4 static let qrViewContentInsets: CGFloat = 8 static let containerHorizontalOffset: CGFloat = 16 static let shareButtonTopOffset: CGFloat = 24 static let shareButtonHeight: CGFloat = 52 static let containerInsets = UIEdgeInsets( - top: 16, + top: 40, left: containerHorizontalOffset, bottom: Constants.shareButtonHeight + shareButtonTopOffset, right: containerHorizontalOffset diff --git a/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift b/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift index c94c5ff578..32d709e743 100644 --- a/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift +++ b/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift @@ -1,11 +1,27 @@ import Operation_iOS protocol QRCreationOperationFactoryProtocol { - func createOperation(payload: Data, qrSize: CGSize) -> BaseOperation + func createOperation( + payload: Data, + qrSize: CGSize + ) -> BaseOperation } final class QRCreationOperationFactory: QRCreationOperationFactoryProtocol { - func createOperation(payload: Data, qrSize: CGSize) -> BaseOperation { - QRCreationOperation(payload: payload, qrSize: qrSize) + private let logoSize: CGSize + + init(logoSize: CGSize = .init(width: 64, height: 64)) { + self.logoSize = logoSize + } + + func createOperation( + payload: Data, + qrSize: CGSize + ) -> BaseOperation { + QRCreationOperation( + payload: payload, + qrSize: qrSize, + logoSize: logoSize + ) } } diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 44821d5e06..09d1512be8 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1765,3 +1765,4 @@ "settings.appearance.token.icons.title" = "Token icons"; "settings.appearance.token.icons.option.white" = "White"; "settings.appearance.token.icons.option.colored" = "Colored"; +"wallet.receive.details.format" = "Send only %@ token and tokens in %@ network to this address, or you might lose your funds"; From d139ae0748ea8a57cc57f76eecc5a4bd5f5eccae Mon Sep 17 00:00:00 2001 From: svojsu Date: Tue, 29 Oct 2024 23:38:39 +0200 Subject: [PATCH 02/12] account address view added --- novawallet.xcodeproj/project.pbxproj | 16 +++-- .../Contents.json | 20 ++++++ .../Contents.json | 20 ++++++ .../Extension/UIKit/RoundedView+Styles.swift | 2 +- .../Extension/UIKit/Style/UILabel+Style.swift | 10 +++ .../ChainAccount/ChainAccountControl.swift | 47 -------------- .../AssetReceive/AssetReceivePresenter.swift | 25 ++++---- .../AssetReceive/AssetReceiveProtocols.swift | 12 ++-- .../AssetReceiveViewController.swift | 41 ++++++++++--- .../AssetReceive/AssetReceiveViewLayout.swift | 42 +++++++++---- .../View/AccountAddressView.swift | 61 +++++++++++++++++++ novawallet/en.lproj/Localizable.strings | 1 + 12 files changed, 206 insertions(+), 91 deletions(-) create mode 100644 novawallet/Assets.xcassets/colors/text/colorTextPrimaryOnWhite.colorset/Contents.json create mode 100644 novawallet/Assets.xcassets/colors/text/colorTextSecondaryOnWhite.colorset/Contents.json delete mode 100644 novawallet/Common/View/ChainAccount/ChainAccountControl.swift create mode 100644 novawallet/Modules/AssetReceive/View/AccountAddressView.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index dd1127b94c..866e445cbd 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -3738,7 +3738,7 @@ 84D9C8F328ADA42F007FB23B /* Data+Chunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D9C8F228ADA42F007FB23B /* Data+Chunk.swift */; }; 84DA03D12758AA6800E8B326 /* BaseAccountImportWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA03D02758AA6800E8B326 /* BaseAccountImportWireframe.swift */; }; 84DA03D427592FAA00E8B326 /* ChainAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA03D327592FAA00E8B326 /* ChainAccountView.swift */; }; - 84DA03D62759341200E8B326 /* ChainAccountControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA03D52759341200E8B326 /* ChainAccountControl.swift */; }; + 84DA03D62759341200E8B326 /* AccountAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA03D52759341200E8B326 /* AccountAddressView.swift */; }; 84DA03D82759362100E8B326 /* ChainAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA03D72759362100E8B326 /* ChainAccountViewModel.swift */; }; 84DA03DB275A31B500E8B326 /* DAppListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA03DA275A31B500E8B326 /* DAppListHeaderView.swift */; }; 84DA3B1224C6D29100B5E27F /* RuntimeVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DA3B1124C6D29100B5E27F /* RuntimeVersion.swift */; }; @@ -8866,7 +8866,7 @@ 84D9C8F228ADA42F007FB23B /* Data+Chunk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Chunk.swift"; sourceTree = ""; }; 84DA03D02758AA6800E8B326 /* BaseAccountImportWireframe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseAccountImportWireframe.swift; sourceTree = ""; }; 84DA03D327592FAA00E8B326 /* ChainAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainAccountView.swift; sourceTree = ""; }; - 84DA03D52759341200E8B326 /* ChainAccountControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainAccountControl.swift; sourceTree = ""; }; + 84DA03D52759341200E8B326 /* AccountAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountAddressView.swift; sourceTree = ""; }; 84DA03D72759362100E8B326 /* ChainAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainAccountViewModel.swift; sourceTree = ""; }; 84DA03DA275A31B500E8B326 /* DAppListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAppListHeaderView.swift; sourceTree = ""; }; 84DA3B1124C6D29100B5E27F /* RuntimeVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeVersion.swift; sourceTree = ""; }; @@ -12542,6 +12542,14 @@ path = Appearance; sourceTree = ""; }; + 2D442CF42CD182F600A0380F /* View */ = { + isa = PBXGroup; + children = ( + 84DA03D52759341200E8B326 /* AccountAddressView.swift */, + ); + path = View; + sourceTree = ""; + }; 2D4F54FF2CCA60C500B65B76 /* AssetsSearchCollectionManager */ = { isa = PBXGroup; children = ( @@ -20492,7 +20500,6 @@ isa = PBXGroup; children = ( 84DA03D327592FAA00E8B326 /* ChainAccountView.swift */, - 84DA03D52759341200E8B326 /* ChainAccountControl.swift */, 84DA03D72759362100E8B326 /* ChainAccountViewModel.swift */, 846B748E28B41C6B00C39B93 /* ChainAccountTableViewCell.swift */, ); @@ -23333,6 +23340,7 @@ EC1A579A3747EB16688DAEBF /* AssetReceive */ = { isa = PBXGroup; children = ( + 2D442CF42CD182F600A0380F /* View */, 88E74E8629538BEB008031A3 /* Model */, 47042BBA5082B1EC1D017B56 /* AssetReceiveProtocols.swift */, 6C1179A25C22AF0875A1ADCD /* AssetReceiveWireframe.swift */, @@ -26012,7 +26020,7 @@ 8425EA9025EA7E5800C307C9 /* ElectedValidatorInfo.swift in Sources */, 0C500B222B05102900ABEE70 /* AssetTxPaymentPallet.swift in Sources */, 84216FCF28264A1E00479375 /* ParaStakingRewardCalculatorEngine.swift in Sources */, - 84DA03D62759341200E8B326 /* ChainAccountControl.swift in Sources */, + 84DA03D62759341200E8B326 /* AccountAddressView.swift in Sources */, 84329ED02832461D0020BC1C /* TimeInterval+Localization.swift in Sources */, 846AF8442525BE0100868F37 /* Price.swift in Sources */, 84468A072866530100BCBE00 /* AssetStorageInfoOperationFactory.swift in Sources */, diff --git a/novawallet/Assets.xcassets/colors/text/colorTextPrimaryOnWhite.colorset/Contents.json b/novawallet/Assets.xcassets/colors/text/colorTextPrimaryOnWhite.colorset/Contents.json new file mode 100644 index 0000000000..7e8f38fe3b --- /dev/null +++ b/novawallet/Assets.xcassets/colors/text/colorTextPrimaryOnWhite.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Assets.xcassets/colors/text/colorTextSecondaryOnWhite.colorset/Contents.json b/novawallet/Assets.xcassets/colors/text/colorTextSecondaryOnWhite.colorset/Contents.json new file mode 100644 index 0000000000..ce333dda06 --- /dev/null +++ b/novawallet/Assets.xcassets/colors/text/colorTextSecondaryOnWhite.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.560", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift b/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift index 7a8d1db8e0..18eddf59be 100644 --- a/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift +++ b/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift @@ -25,8 +25,8 @@ extension RoundedView { func applyControlBackgroundStyle() { strokeColor = R.color.colorContainerBorder()! highlightedStrokeColor = .clear + shadowOpacity = .zero fillColor = .clear - highlightedFillColor = R.color.colorCellBackgroundPressed()! } func applyBorderBackgroundStyle() { diff --git a/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift b/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift index a8a2712ce9..160c121009 100644 --- a/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift +++ b/novawallet/Common/Extension/UIKit/Style/UILabel+Style.swift @@ -31,6 +31,11 @@ extension UILabel.Style { font: .regularFootnote ) + static let footnoteSecondaryOnWhite = UILabel.Style( + textColor: R.color.colorTextSecondaryOnWhite(), + font: .regularFootnote + ) + static let semiboldSubhedlineSecondary = UILabel.Style( textColor: R.color.colorTextSecondary(), font: .semiBoldSubheadline @@ -151,6 +156,11 @@ extension UILabel.Style { font: .regularSubheadline ) + static let regularSubhedlinePrimaryOnWhite = UILabel.Style( + textColor: R.color.colorTextPrimaryOnWhite(), + font: .regularSubheadline + ) + static let regularSubhedlineInactive = UILabel.Style( textColor: R.color.colorIconInactive(), font: .regularSubheadline diff --git a/novawallet/Common/View/ChainAccount/ChainAccountControl.swift b/novawallet/Common/View/ChainAccount/ChainAccountControl.swift deleted file mode 100644 index 2e8079bc2c..0000000000 --- a/novawallet/Common/View/ChainAccount/ChainAccountControl.swift +++ /dev/null @@ -1,47 +0,0 @@ -import Foundation -import SoraUI - -final class ChainAccountControl: BackgroundedContentControl { - let chainAccountView = ChainAccountView() - let roundedBackgroundView: RoundedView = { - let roundedView = UIFactory.default.createRoundedBackgroundView() - roundedView.applyControlBackgroundStyle() - return roundedView - }() - - override init(frame: CGRect) { - super.init(frame: frame) - - contentInsets = UIEdgeInsets( - top: 7.0, - left: 18.0, - bottom: 7.0, - right: 18.0 - ) - - backgroundView = roundedBackgroundView - contentView = chainAccountView - contentView?.isUserInteractionEnabled = false - backgroundView?.isUserInteractionEnabled = false - - contentView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] - } - - override func layoutSubviews() { - super.layoutSubviews() - - backgroundView?.frame = bounds - - contentView?.frame = CGRect( - x: bounds.minX + contentInsets.left, - y: bounds.minY + contentInsets.top, - width: bounds.width - contentInsets.left - contentInsets.right, - height: bounds.height - contentInsets.top - contentInsets.bottom - ) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} diff --git a/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift b/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift index 8ed1819266..78a5fe67f2 100644 --- a/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift +++ b/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift @@ -87,18 +87,16 @@ extension AssetReceivePresenter: AssetReceivePresenterProtocol { ) } - func presentAccountOptions() { - guard let view = view, - let address = account?.chainAccount.toAddress(), - let chain = chain else { + func copyAddress() { + guard let address = account?.chainAccount.toAddress() else { return } - wireframe.presentAccountOptions( - from: view, - address: address, - chain: chain, - locale: localizationManager.selectedLocale + UIPasteboard.general.string = address + + wireframe.presentSuccessNotification( + R.string.localizable.commonAddressCoppied(), + from: view ) } } @@ -114,13 +112,14 @@ extension AssetReceivePresenter: AssetReceiveInteractorOutputProtocol { provideNetwork() - let chainAccountViewModel = createChainAccountViewModel( - for: account.chainAccount.accountId, - chain: chain + let addressViewModel = AccountAddressViewModel( + walletName: account.chainAccount.name, + address: account.chainAccount.toAddress() ) view?.didReceive( - chainAccountViewModel: chainAccountViewModel, + addressViewModel: addressViewModel, + networkName: chain.name, token: token ) } diff --git a/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift b/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift index 83c505264a..cf24422c54 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift @@ -2,7 +2,11 @@ import UIKit protocol AssetReceiveViewProtocol: ControllerBackedProtocol { func didReceive(networkViewModel: NetworkViewModel) - func didReceive(chainAccountViewModel: ChainAccountViewModel, token: String) + func didReceive( + addressViewModel: AccountAddressViewModel, + networkName: String, + token: String + ) func didReceive(qrImage: UIImage) } @@ -10,7 +14,7 @@ protocol AssetReceivePresenterProtocol: AnyObject { func setup() func set(qrCodeSize: CGSize) func share() - func presentAccountOptions() + func copyAddress() } protocol AssetReceiveInteractorInputProtocol: AnyObject { @@ -24,5 +28,5 @@ protocol AssetReceiveInteractorOutputProtocol: AnyObject { func didReceive(error: AssetReceiveInteractorError) } -protocol AssetReceiveWireframeProtocol: AnyObject, SharingPresentable, AddressOptionsPresentable, - ErrorPresentable, AlertPresentable, CommonRetryable {} +protocol AssetReceiveWireframeProtocol: AnyObject, SharingPresentable, + ErrorPresentable, AlertPresentable, CommonRetryable, ModalAlertPresenting {} diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift index 83b503db01..ed0e006dba 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift @@ -45,14 +45,27 @@ final class AssetReceiveViewController: UIViewController, ViewHolder { } private func setupLocalization() { + let languages = selectedLocale.rLanguages + rootView.shareButton.imageWithTitleView?.title = R.string.localizable.walletReceiveShareTitle( - preferredLanguages: selectedLocale.rLanguages + preferredLanguages: languages ) + rootView.accountAddressView.copyButton.imageWithTitleView?.title = R.string.localizable.commonCopyAddress( + preferredLanguages: languages + ).capitalized } private func setupHandlers() { - rootView.shareButton.addTarget(self, action: #selector(didTapShare), for: .touchUpInside) - rootView.accountDetailsView.addTarget(self, action: #selector(didTapOnAccount), for: .touchUpInside) + rootView.shareButton.addTarget( + self, + action: #selector(didTapShare), + for: .touchUpInside + ) + rootView.accountAddressView.copyButton.addTarget( + self, + action: #selector(didTapCopyAddress), + for: .touchUpInside + ) } private func updateTitleDetails( @@ -81,19 +94,29 @@ final class AssetReceiveViewController: UIViewController, ViewHolder { presenter.share() } - @objc private func didTapOnAccount() { - presenter.presentAccountOptions() + @objc private func didTapCopyAddress() { + presenter.copyAddress() } } -extension AssetReceiveViewController: AssetReceiveViewProtocol { - func didReceive(chainAccountViewModel: ChainAccountViewModel, token: String) { - rootView.accountDetailsView.chainAccountView.bind(viewModel: chainAccountViewModel) +struct AccountAddressViewModel { + let walletName: String? + let address: String? +} +extension AssetReceiveViewController: AssetReceiveViewProtocol { + func didReceive( + addressViewModel: AccountAddressViewModel, + networkName: String, + token: String + ) { updateTitleDetails( - chainName: chainAccountViewModel.networkName, + chainName: networkName, token: token ) + + rootView.accountAddressView.titleLabel.text = addressViewModel.walletName + rootView.accountAddressView.addressLabel.text = addressViewModel.address } func didReceive(qrImage: UIImage) { diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift index 575f2f6619..6faee5e5ab 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift @@ -1,4 +1,5 @@ import UIKit +import SoraUI import SnapKit final class AssetReceiveViewLayout: UIView { @@ -10,11 +11,14 @@ final class AssetReceiveViewLayout: UIView { return view }() + let qrContainerView: RoundedView = .create { view in + view.fillColor = .white + view.cornerRadius = Constants.qrContainerCornerRadius + } + let chainView = AssetListChainView() - let accountDetailsView: ChainAccountControl = .create { - $0.chainAccountView.actionIconView.image = R.image.iconMore()?.tinted(with: R.color.colorIconSecondary()!) - } + let accountAddressView = AccountAddressView() let titleLabel: UILabel = .create { view in view.apply(style: .title3Primary) @@ -27,7 +31,11 @@ final class AssetReceiveViewLayout: UIView { view.textAlignment = .center } - let qrView: QRDisplayView = .create { $0.contentInsets = Constants.qrViewContentInsets } + let qrView: QRDisplayView = .create { + $0.contentInsets = Constants.qrViewContentInsets + $0.backgroundView.shadowOpacity = .zero + } + let shareButton: TriangularedButton = .create { $0.applyDefaultStyle() } override init(frame: CGRect) { @@ -50,14 +58,24 @@ final class AssetReceiveViewLayout: UIView { } containerView.stackView.addArrangedSubview(titleLabel) containerView.stackView.addArrangedSubview(detailsLabel) - containerView.stackView.addArrangedSubview(qrView) + containerView.stackView.addArrangedSubview(qrContainerView) + + qrContainerView.addSubview(qrView) + qrContainerView.addSubview(accountAddressView) qrView.snp.makeConstraints { - $0.width.equalToSuperview().multipliedBy(Constants.qrViewSizeRatio) + $0.width.equalTo(containerView).multipliedBy(Constants.qrViewSizeRatio) $0.width.equalTo(Constants.qrViewPlaceHolderWidth).priority(.high) + $0.top.leading.trailing.equalToSuperview() $0.height.equalTo(qrView.snp.width) } + accountAddressView.snp.makeConstraints { make in + make.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) + make.top.equalTo(qrView.snp.bottom).offset(Constants.qrViewContentInsets) + make.bottom.equalToSuperview().inset(Constants.addressViewBottomInset) + } + containerView.stackView.setCustomSpacing( Constants.titleDetailsVerticalSpace, after: titleLabel @@ -69,28 +87,26 @@ final class AssetReceiveViewLayout: UIView { addSubview(shareButton) shareButton.snp.makeConstraints { - $0.top.equalTo(qrView.snp.bottom).offset(Constants.shareButtonTopOffset).priority(.high) - $0.leading.equalTo(qrView.snp.leading) - $0.trailing.equalTo(qrView.snp.trailing) + $0.leading.trailing.equalToSuperview().inset(UIConstants.horizontalInset) $0.height.equalTo(Constants.shareButtonHeight) - $0.bottom.lessThanOrEqualTo(safeAreaLayoutGuide.snp.bottom) + $0.bottom.equalTo(safeAreaLayoutGuide.snp.bottom) } } } extension AssetReceiveViewLayout { enum Constants { - static let qrViewSizeRatio: CGFloat = 0.75 + static let qrContainerCornerRadius: CGFloat = 16 + static let qrViewSizeRatio: CGFloat = 0.8 static let qrViewPlaceHolderWidth: CGFloat = 280 static let qrCodeMinimumWidth: CGFloat = 120 - static let accountDetailsViewHeight: CGFloat = 52 - static let accountDetailsTitleVerticalSpace: CGFloat = 52 static let detailsQRVerticalSpace: CGFloat = 36 static let titleDetailsVerticalSpace: CGFloat = 4 static let qrViewContentInsets: CGFloat = 8 static let containerHorizontalOffset: CGFloat = 16 static let shareButtonTopOffset: CGFloat = 24 static let shareButtonHeight: CGFloat = 52 + static let addressViewBottomInset: CGFloat = 12 static let containerInsets = UIEdgeInsets( top: 40, left: containerHorizontalOffset, diff --git a/novawallet/Modules/AssetReceive/View/AccountAddressView.swift b/novawallet/Modules/AssetReceive/View/AccountAddressView.swift new file mode 100644 index 0000000000..8a1120e106 --- /dev/null +++ b/novawallet/Modules/AssetReceive/View/AccountAddressView.swift @@ -0,0 +1,61 @@ +import Foundation +import SoraUI + +final class AccountAddressView: UIView { + let titleLabel: UILabel = .create { view in + view.apply(style: .regularSubhedlinePrimaryOnWhite) + view.textAlignment = .center + } + + let addressLabel: UILabel = .create { view in + view.apply(style: .footnoteSecondaryOnWhite) + view.textAlignment = .center + view.numberOfLines = 0 + } + + let copyButton: TriangularedButton = .create { view in + view.applyEnabledStyle( + colored: .clear, + textColor: R.color.colorButtonTextAccent()! + ) + + view.imageWithTitleView?.iconImage = R.image.iconActionCopy()?.tinted( + with: R.color.colorIconAccent()! + )?.kf.resize(to: .init(width: 16, height: 16)) + + view.imageWithTitleView?.titleFont = .caption1 + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + let container = UIStackView.vStack( + alignment: .center, + spacing: 4, + [ + titleLabel, + addressLabel, + copyButton + ] + ) + + addSubview(container) + + container.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + copyButton.snp.makeConstraints { make in + make.height.equalTo(32) + } + } +} diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index 09d1512be8..6bf7868edc 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1766,3 +1766,4 @@ "settings.appearance.token.icons.option.white" = "White"; "settings.appearance.token.icons.option.colored" = "Colored"; "wallet.receive.details.format" = "Send only %@ token and tokens in %@ network to this address, or you might lose your funds"; +"common.address.coppied" = "Address copied"; From 9b3516e4ced76c46abbb0b03aa1dc92ac54497e4 Mon Sep 17 00:00:00 2001 From: svojsu Date: Tue, 29 Oct 2024 23:45:59 +0200 Subject: [PATCH 03/12] add icon to share button --- novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift index 6faee5e5ab..4ed21afe00 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift @@ -36,7 +36,10 @@ final class AssetReceiveViewLayout: UIView { $0.backgroundView.shadowOpacity = .zero } - let shareButton: TriangularedButton = .create { $0.applyDefaultStyle() } + let shareButton: TriangularedButton = .create { + $0.applyDefaultStyle() + $0.imageWithTitleView?.iconImage = R.image.iconShare() + } override init(frame: CGRect) { super.init(frame: frame) From be840122e08ab7a457ef37109ae67daede8efa35 Mon Sep 17 00:00:00 2001 From: svojsu Date: Wed, 30 Oct 2024 10:28:42 +0200 Subject: [PATCH 04/12] embed asset logo in QR code --- .../QRCreation/QRCreationOperation.swift | 79 ++++++++++++++++--- .../AssetReceive/AssetReceiveInteractor.swift | 25 ++++++ .../AssetReceiveViewFactory.swift | 1 + .../QRCreationOperationFactory.swift | 3 + .../ParitySignerTxQrInteractor.swift | 7 +- 5 files changed, 100 insertions(+), 15 deletions(-) diff --git a/novawallet/Common/QRCreation/QRCreationOperation.swift b/novawallet/Common/QRCreation/QRCreationOperation.swift index 6aabc1829c..c6f8e84851 100644 --- a/novawallet/Common/QRCreation/QRCreationOperation.swift +++ b/novawallet/Common/QRCreation/QRCreationOperation.swift @@ -1,6 +1,7 @@ import Foundation import CoreImage import Operation_iOS +import Kingfisher import QRCode enum QRCreationOperationError: Error { @@ -13,25 +14,30 @@ final class QRCreationOperation: BaseOperation { let payloadClosure: () throws -> Data let qrSize: CGSize let logoSize: CGSize? + let logoURL: URL? init( payload: Data, qrSize: CGSize, + logoURL: URL?, logoSize: CGSize? = nil ) { payloadClosure = { payload } self.qrSize = qrSize self.logoSize = logoSize + self.logoURL = logoURL super.init() } init( qrSize: CGSize, + logoURL: URL?, logoSize: CGSize?, payloadClosure: @escaping () throws -> Data ) { self.qrSize = qrSize + self.logoURL = logoURL self.logoSize = logoSize self.payloadClosure = payloadClosure } @@ -46,28 +52,75 @@ final class QRCreationOperation: BaseOperation { qrDoc.design.shape.offPixels = nil qrDoc.design.style.offPixels = nil - if let logoSize { - let scaledSize = CGSize( - width: logoSize.width * UIScreen.main.scale, - height: logoSize.height * UIScreen.main.scale - ) + let scale = UIScreen.main.scale + + let scaledSize = CGSize( + width: qrSize.width * scale, + height: qrSize.height * scale + ) + + let qrCreateImageClosure: () throws -> Void = { [weak self] in + guard let self else { return } - guard let image = UIImage.background(from: .white, size: scaledSize)?.cgImage else { - return + guard let cgImage = qrDoc.cgImage(scaledSize) else { + throw BarcodeCreationError.bitmapImageCreationFailed } - qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: image) + callback(.success(UIImage(cgImage: cgImage))) + } + + guard let logoSize else { + try qrCreateImageClosure() + return + } + + try retrieveImage( + of: logoSize, + scale: scale, + using: logoURL + ) { logoImage in + qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: logoImage) + + try qrCreateImageClosure() } + } + + private func retrieveImage( + of size: CGSize, + scale: CGFloat, + using url: URL?, + completion: @escaping (CGImage) throws -> Void + ) throws { + let defaultTokenImage = R.image.iconDefaultToken()! let scaledSize = CGSize( - width: qrSize.width * UIScreen.main.scale, - height: qrSize.height * UIScreen.main.scale + width: size.width * scale, + height: size.height * scale ) - guard let cgImage = qrDoc.cgImage(scaledSize) else { - throw BarcodeCreationError.bitmapImageCreationFailed + guard let url else { + try completion( + defaultTokenImage.kf.resize(to: scaledSize).cgImage! + ) + + return } - callback(.success(UIImage(cgImage: cgImage))) + let options: KingfisherOptionsInfo = [.processor(SVGImageProcessor())] + + KingfisherManager.shared.downloader.downloadImage(with: url, options: options) { result in + let resultImage: UIImage + + switch result { + case let .success(imageResult) where imageResult.image.cgImage != nil: + resultImage = imageResult.image + default: + resultImage = defaultTokenImage + } + + let resizedImage = resultImage.kf.resize(to: scaledSize) + + try? completion(resizedImage.cgImage!) + } } } diff --git a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift index 79979249ed..07d6083cf4 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift @@ -1,6 +1,22 @@ import UIKit import Operation_iOS +enum AssetIconURLFactory { + static func createURL( + for iconName: String?, + iconAppearance: AppearanceIconsOptions + ) -> URL? { + guard let iconName else { return nil } + + return switch iconAppearance { + case .white: + URL(string: ApplicationConfig.shared.whiteAppearanceIconsPath + iconName) + case .colored: + URL(string: ApplicationConfig.shared.coloredAppearanceIconsPath + iconName) + } + } +} + final class AssetReceiveInteractor: AnyCancellableCleaning { weak var presenter: AssetReceiveInteractorOutputProtocol! @@ -8,6 +24,7 @@ final class AssetReceiveInteractor: AnyCancellableCleaning { let qrCoderFactory: NovaWalletQRCoderFactoryProtocol let qrCodeCreationOperationFactory: QRCreationOperationFactoryProtocol let metaChainAccountResponse: MetaChainAccountResponse + let appearanceFacade: AppearanceFacadeProtocol private let operationQueue: OperationQueue private var currentQRCodeOperation: CancellableCall? @@ -17,12 +34,14 @@ final class AssetReceiveInteractor: AnyCancellableCleaning { chainAsset: ChainAsset, qrCoderFactory: NovaWalletQRCoderFactoryProtocol, qrCodeCreationOperationFactory: QRCreationOperationFactoryProtocol, + appearanceFacade: AppearanceFacadeProtocol, operationQueue: OperationQueue ) { self.metaChainAccountResponse = metaChainAccountResponse self.chainAsset = chainAsset self.qrCoderFactory = qrCoderFactory self.qrCodeCreationOperationFactory = qrCodeCreationOperationFactory + self.appearanceFacade = appearanceFacade self.operationQueue = operationQueue } @@ -41,8 +60,14 @@ final class AssetReceiveInteractor: AnyCancellableCleaning { return } + let iconURL = AssetIconURLFactory.createURL( + for: chainAsset.asset.icon, + iconAppearance: appearanceFacade.selectedIconAppearance + ) + let qrCreationOperation = qrCodeCreationOperationFactory.createOperation( payload: payload, + logoURL: iconURL, qrSize: size ) diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift index 84c042074d..682f38f64e 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift @@ -18,6 +18,7 @@ struct AssetReceiveViewFactory { chainAsset: chainAsset, qrCoderFactory: qrCoderFactory, qrCodeCreationOperationFactory: QRCreationOperationFactory(), + appearanceFacade: AppearanceFacade.shared, operationQueue: OperationManagerFacade.sharedDefaultQueue ) let wireframe = AssetReceiveWireframe() diff --git a/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift b/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift index 32d709e743..c03bd4dd02 100644 --- a/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift +++ b/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift @@ -3,6 +3,7 @@ import Operation_iOS protocol QRCreationOperationFactoryProtocol { func createOperation( payload: Data, + logoURL: URL?, qrSize: CGSize ) -> BaseOperation } @@ -16,11 +17,13 @@ final class QRCreationOperationFactory: QRCreationOperationFactoryProtocol { func createOperation( payload: Data, + logoURL: URL?, qrSize: CGSize ) -> BaseOperation { QRCreationOperation( payload: payload, qrSize: qrSize, + logoURL: logoURL, logoSize: logoSize ) } diff --git a/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift b/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift index e2f9cd22c7..ec5bf721e0 100644 --- a/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift +++ b/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift @@ -36,7 +36,10 @@ final class ParitySignerTxQrInteractor { self.operationQueue = operationQueue } - private func provideTransactionCode(for size: CGSize, account: ChainAccountResponse) { + private func provideTransactionCode( + for size: CGSize, + account: ChainAccountResponse + ) { let messageWrapper = messageOperationFactory.createTransaction( for: signingData, accountId: account.accountId, @@ -56,7 +59,7 @@ final class ParitySignerTxQrInteractor { let paylods = try qrPayloadWrapper.targetOperation.extractNoCancellableResultData() return paylods.map { payload in - let operation = QRCreationOperation(payload: payload, qrSize: size) + let operation = QRCreationOperation(payload: payload, qrSize: size, logoURL: nil) return CompoundOperationWrapper(targetOperation: operation) } }.longrunOperation() From 60b1342a1505739bd7634e2c8fc339554ed62bbe Mon Sep 17 00:00:00 2001 From: svojsu Date: Wed, 30 Oct 2024 18:18:08 +0200 Subject: [PATCH 05/12] cache logic for QR creation --- novawallet.xcodeproj/project.pbxproj | 8 + .../Extension/UIKit/UIImage+Drawing.swift | 28 +++ .../Common/QRCreation/QRCodeFactory.swift | 194 ++++++++++++++++++ .../QRCreation/QRCreationOperation.swift | 126 +++++++++--- .../AssetReceive/AssetReceiveInteractor.swift | 61 ++---- .../AssetReceiveViewFactory.swift | 11 +- .../Model/AssetIconURLFactory.swift | 37 ++++ .../QRCreationOperationFactory.swift | 17 +- .../ParitySignerTxQrInteractor.swift | 2 +- 9 files changed, 391 insertions(+), 93 deletions(-) create mode 100644 novawallet/Common/QRCreation/QRCodeFactory.swift create mode 100644 novawallet/Modules/AssetReceive/Model/AssetIconURLFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 866e445cbd..8c81d535ae 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -1070,6 +1070,8 @@ 2DAF539E2C1B2AA00076B4B6 /* NodePingOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAF539D2C1B2AA00076B4B6 /* NodePingOperationFactory.swift */; }; 2DB18C8F2C36AB1C00A93C75 /* LightChainsFetchFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB18C8E2C36AB1C00A93C75 /* LightChainsFetchFactory.swift */; }; 2DB18C922C36D1BC00A93C75 /* PreConfiguredChainFetchFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB18C912C36D1BC00A93C75 /* PreConfiguredChainFetchFactory.swift */; }; + 2DBB28852CD2811200DFA0AD /* QRCodeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28842CD2811200DFA0AD /* QRCodeFactory.swift */; }; + 2DBB28872CD2934500DFA0AD /* AssetIconURLFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28862CD2934500DFA0AD /* AssetIconURLFactory.swift */; }; 2DC075E32BF24AB400868563 /* CheckBoxIconDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC075E22BF24AB400868563 /* CheckBoxIconDetailsView.swift */; }; 2DC075E92BF4B97900868563 /* BackupAttentionTableTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC075E82BF4B97900868563 /* BackupAttentionTableTitleView.swift */; }; 2DCC875B2C5820BA0028C3CA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849013D424A927E2008F705E /* Logger.swift */; }; @@ -6247,6 +6249,8 @@ 2DAF539D2C1B2AA00076B4B6 /* NodePingOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePingOperationFactory.swift; sourceTree = ""; }; 2DB18C8E2C36AB1C00A93C75 /* LightChainsFetchFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightChainsFetchFactory.swift; sourceTree = ""; }; 2DB18C912C36D1BC00A93C75 /* PreConfiguredChainFetchFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreConfiguredChainFetchFactory.swift; sourceTree = ""; }; + 2DBB28842CD2811200DFA0AD /* QRCodeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeFactory.swift; sourceTree = ""; }; + 2DBB28862CD2934500DFA0AD /* AssetIconURLFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetIconURLFactory.swift; sourceTree = ""; }; 2DC075E22BF24AB400868563 /* CheckBoxIconDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBoxIconDetailsView.swift; sourceTree = ""; }; 2DC075E82BF4B97900868563 /* BackupAttentionTableTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAttentionTableTitleView.swift; sourceTree = ""; }; 2DC70DB97D2E9350022A899B /* TokensManagePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManagePresenter.swift; sourceTree = ""; }; @@ -19793,6 +19797,7 @@ isa = PBXGroup; children = ( 84BC703E289DBE6C008A9758 /* QRCreationOperation.swift */, + 2DBB28842CD2811200DFA0AD /* QRCodeFactory.swift */, 84BC7040289DBF62008A9758 /* QRDisplayView.swift */, 2D442CF22CD103ED00A0380F /* BarcodeCreationError.swift */, ); @@ -21880,6 +21885,7 @@ children = ( 84ABB3302A16150400B5E95A /* AccountShareFactory.swift */, 84ABB32F2A16150400B5E95A /* AssetReceiveInfo.swift */, + 2DBB28862CD2934500DFA0AD /* AssetIconURLFactory.swift */, 84ABB3312A16150400B5E95A /* LegacyWalletQRCoderFactory.swift */, 88E74E8729538BF8008031A3 /* AssetReceiveInteractorError.swift */, 88E74E8B29538C36008031A3 /* QRCodeInfo.swift */, @@ -25678,6 +25684,7 @@ 0C3205E82A898195002EB914 /* EvmValidationErrorPresentable.swift in Sources */, 84CE69E82566750D00559427 /* ByteLengthProcessor.swift in Sources */, 8499FEDA27BFDB8C00712589 /* NFTStreamableSource.swift in Sources */, + 2DBB28852CD2811200DFA0AD /* QRCodeFactory.swift in Sources */, 0CE360212C33F398006A6CE4 /* GraphModel+Dijkstra.swift in Sources */, 849976BE27B269A400B14A6C /* DAppTransports.swift in Sources */, 842876A724AE049B00D91AD8 /* SelectionListProtocols.swift in Sources */, @@ -27660,6 +27667,7 @@ 2D008A682CC57BFB00BFEE99 /* AssetListCollectionViewDelegate.swift in Sources */, 84FD91B029B08F7A007851D3 /* BaseTableSearchViewController.swift in Sources */, 1BEADE77C6236CB3BF719A47 /* CrowdloanContributionSetupViewFactory.swift in Sources */, + 2DBB28872CD2934500DFA0AD /* AssetIconURLFactory.swift in Sources */, 7719019F2AE6C9DC00D9C918 /* SwapElementView.swift in Sources */, 845353BD2886EB1A006C871A /* ButtonLargeControl.swift in Sources */, 8466781827EC9CDA007935D3 /* PersistTransferDetails.swift in Sources */, diff --git a/novawallet/Common/Extension/UIKit/UIImage+Drawing.swift b/novawallet/Common/Extension/UIKit/UIImage+Drawing.swift index a4e4a4b43c..bef402c07c 100644 --- a/novawallet/Common/Extension/UIKit/UIImage+Drawing.swift +++ b/novawallet/Common/Extension/UIKit/UIImage+Drawing.swift @@ -1,6 +1,11 @@ import UIKit extension UIImage { + enum Shape { + case rectangle + case circle + } + static func background( from color: UIColor, size: CGSize = CGSize(width: 1.0, height: 1.0), @@ -25,6 +30,29 @@ extension UIImage { return image } + func redrawWithBackground( + color: UIColor, + shape: Shape = .rectangle + ) -> UIImage { + let renderer = UIGraphicsImageRenderer(size: size, format: UIGraphicsImageRendererFormat.default()) + + return renderer.image { context in + let rect = CGRect(origin: .zero, size: size) + + color.setFill() + + switch shape { + case .rectangle: + context.fill(rect) + + case .circle: + context.cgContext.fillEllipse(in: rect) + } + + self.draw(in: rect) + } + } + func crop(targetSize: CGSize, cornerRadius: CGFloat, contentScale: CGFloat) -> UIImage? { guard size.width > 0, size.height > 0 else { return nil diff --git a/novawallet/Common/QRCreation/QRCodeFactory.swift b/novawallet/Common/QRCreation/QRCodeFactory.swift new file mode 100644 index 0000000000..39a6bf194c --- /dev/null +++ b/novawallet/Common/QRCreation/QRCodeFactory.swift @@ -0,0 +1,194 @@ +import Foundation +import Kingfisher +import Operation_iOS + +protocol QRCodeFactoryProtocol { + func createQRCode( + with payload: Data, + logoInfo: QRLogoInfo?, + qrSize: CGSize, + completion: @escaping (QRCodeFactory.Result) -> Void + ) +} + +final class QRCodeFactory { + enum Result: Equatable { + case full(UIImage) + case noLogo(UIImage) + + var image: UIImage { + switch self { + case let .full(image), let .noLogo(image): + return image + } + } + } + + let operationFactory: QRCreationOperationFactoryProtocol + let operationQueue: OperationQueue + + init( + operationFactory: QRCreationOperationFactoryProtocol, + operationQueue: OperationQueue + ) { + self.operationFactory = operationFactory + self.operationQueue = operationQueue + } +} + +// MARK: QRCodeFactoryProtocol + +extension QRCodeFactory: QRCodeFactoryProtocol { + func createQRCode( + with payload: Data, + logoInfo: QRLogoInfo?, + qrSize: CGSize, + completion: @escaping (Result) -> Void + ) { + let cache = KingfisherManager.shared.cache + + var cached: Bool = { + guard let logoInfo, let cacheKey = logoInfo.type?.cacheKey else { return false } + + let cachedType = cache.imageCachedType(forKey: cacheKey) + + return cachedType.cached + }() + + var usesCache: Bool { logoInfo?.type?.cacheKey != nil } + + guard let logoInfo else { + return + } + + if !cached, usesCache { + createQRCodes( + using: payload, + logoInfo: logoInfo, + qrSize: qrSize, + completion: completion + ) + } else if let cacheKey = logoInfo.type?.cacheKey { + createQRCode( + using: cache, + cacheKey: cacheKey, + payload: payload, + logoInfo: logoInfo, + qrSize: qrSize, + completion: completion + ) + } else { + createQRCode( + using: payload, + logoInfo: logoInfo, + qrSize: qrSize, + completion: completion + ) + } + } +} + +// MARK: Private + +private extension QRCodeFactory { + func createQRCode( + using payload: Data, + logoInfo: QRLogoInfo, + qrSize: CGSize, + completion: @escaping (Result) -> Void + ) { + let operation = operationFactory.createOperation( + payload: payload, + logoInfo: logoInfo, + qrSize: qrSize + ) + + execute( + [(operation: operation, withLogo: true)], + completion: completion + ) + } + + func createQRCode( + using cache: ImageCache, + cacheKey: String, + payload: Data, + logoInfo: QRLogoInfo, + qrSize: CGSize, + completion: @escaping (Result) -> Void + ) { + cache.retrieveImage(forKey: cacheKey) { [weak self] result in + guard let self else { return } + + var updatedLogoInfo: QRLogoInfo? = logoInfo + + if case let .success(cacheResult) = result, let image = cacheResult.image { + updatedLogoInfo = QRLogoInfo( + size: logoInfo.size, + type: .local(image) + ) + } + + let operation = operationFactory.createOperation( + payload: payload, + logoInfo: updatedLogoInfo, + qrSize: qrSize + ) + + execute( + [(operation: operation, withLogo: true)], + completion: completion + ) + } + } + + func createQRCodes( + using payload: Data, + logoInfo: QRLogoInfo, + qrSize: CGSize, + completion: @escaping (Result) -> Void + ) { + let noLogoQROperation = operationFactory.createOperation( + payload: payload, + logoInfo: QRLogoInfo(size: logoInfo.size, type: nil), + qrSize: qrSize + ) + + let embeddedLogoQROperation = operationFactory.createOperation( + payload: payload, + logoInfo: logoInfo, + qrSize: qrSize + ) + + let operations = [ + (operation: noLogoQROperation, withLogo: false), + (operation: embeddedLogoQROperation, withLogo: true) + ] + + execute(operations, completion: completion) + } + + func execute( + _ operations: [(operation: QRCreationOperation, withLogo: Bool)], + completion: @escaping (Result) -> Void + ) { + operations.forEach { operation in + novawallet.execute( + operation: operation.operation, + inOperationQueue: operationQueue, + runningCallbackIn: .main + ) { result in + switch result { + case let .success(qrCodeImage): + let qrCreationResult: Result = operation.withLogo + ? .full(qrCodeImage) + : .noLogo(qrCodeImage) + + completion(qrCreationResult) + case let .failure(error): + print(error) + } + } + } + } +} diff --git a/novawallet/Common/QRCreation/QRCreationOperation.swift b/novawallet/Common/QRCreation/QRCreationOperation.swift index c6f8e84851..c3aacb5a49 100644 --- a/novawallet/Common/QRCreation/QRCreationOperation.swift +++ b/novawallet/Common/QRCreation/QRCreationOperation.swift @@ -10,35 +10,60 @@ enum QRCreationOperationError: Error { case bitmapImageCreationFailed } +enum QRLogoType { + case remoteColored(URL?) + case remoteTransparent(URL?) + case local(UIImage) + + var url: URL? { + switch self { + case let .remoteColored(url), let .remoteTransparent(url): + return url + default: + return nil + } + } + + var cacheKey: String? { + guard let url else { return nil } + + return url.absoluteString + String(describing: self) + } +} + +struct QRLogoInfo { + let size: CGSize + let type: QRLogoType? + + var url: URL? { + type?.url + } +} + final class QRCreationOperation: BaseOperation { let payloadClosure: () throws -> Data let qrSize: CGSize - let logoSize: CGSize? - let logoURL: URL? + let logoInfo: QRLogoInfo? init( payload: Data, qrSize: CGSize, - logoURL: URL?, - logoSize: CGSize? = nil + logoInfo: QRLogoInfo? = nil ) { payloadClosure = { payload } self.qrSize = qrSize - self.logoSize = logoSize - self.logoURL = logoURL + self.logoInfo = logoInfo super.init() } init( qrSize: CGSize, - logoURL: URL?, - logoSize: CGSize?, + logoInfo: QRLogoInfo? = nil, payloadClosure: @escaping () throws -> Data ) { self.qrSize = qrSize - self.logoURL = logoURL - self.logoSize = logoSize + self.logoInfo = logoInfo self.payloadClosure = payloadClosure } @@ -69,58 +94,93 @@ final class QRCreationOperation: BaseOperation { callback(.success(UIImage(cgImage: cgImage))) } - guard let logoSize else { + guard let logoInfo else { try qrCreateImageClosure() return } - try retrieveImage( - of: logoSize, - scale: scale, - using: logoURL - ) { logoImage in - qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: logoImage) + if case let .local(image) = logoInfo.type { + qrDoc.logoTemplate = QRCode.LogoTemplate.SquareCenter( + image: image.cgImage!, + inset: 0 + ) try qrCreateImageClosure() + } else { + try downloadImage( + using: logoInfo, + scale: scale + ) { logoImage in + let inset: CGFloat = switch logoInfo.type { + case .remoteTransparent: 20 + default: 0 + } + + qrDoc.logoTemplate = QRCode.LogoTemplate.SquareCenter( + image: logoImage, + inset: inset + ) + + try qrCreateImageClosure() + } } } - private func retrieveImage( - of size: CGSize, + private func downloadImage( + using logoInfo: QRLogoInfo, scale: CGFloat, - using url: URL?, completion: @escaping (CGImage) throws -> Void ) throws { - let defaultTokenImage = R.image.iconDefaultToken()! - let scaledSize = CGSize( - width: size.width * scale, - height: size.height * scale + width: logoInfo.size.width * scale, + height: logoInfo.size.height * scale ) - guard let url else { - try completion( - defaultTokenImage.kf.resize(to: scaledSize).cgImage! - ) + let defaultImage = UIImage.background(from: .white, size: scaledSize)! + + guard let url = logoInfo.url else { + try completion(defaultImage.cgImage!) return } - let options: KingfisherOptionsInfo = [.processor(SVGImageProcessor())] + let options: KingfisherOptionsInfo = [ + .processor(ResizingImageProcessor(referenceSize: scaledSize)), + .processor(SVGImageProcessor()) + ] KingfisherManager.shared.downloader.downloadImage(with: url, options: options) { result in - let resultImage: UIImage + var resultImage: UIImage switch result { case let .success(imageResult) where imageResult.image.cgImage != nil: resultImage = imageResult.image default: - resultImage = defaultTokenImage + resultImage = defaultImage } - let resizedImage = resultImage.kf.resize(to: scaledSize) + let sizeBeforeProcessing = resultImage.size - try? completion(resizedImage.cgImage!) + if case .remoteTransparent = logoInfo.type { + resultImage = resultImage.redrawWithBackground( + color: R.color.colorTextPrimaryOnWhite()!, + shape: .circle + ) + } + + if let cacheKey = logoInfo.type?.cacheKey { + KingfisherManager.shared.cache.store( + resultImage, + forKey: cacheKey, + options: KingfisherParsedOptionsInfo(nil) + ) + } + + try? completion(resultImage.cgImage!) } } } + +extension CGSize { + static let qrLogoSize: CGSize = .init(width: 80, height: 80) +} diff --git a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift index 07d6083cf4..6f9fbc0cb2 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift @@ -1,28 +1,12 @@ import UIKit import Operation_iOS -enum AssetIconURLFactory { - static func createURL( - for iconName: String?, - iconAppearance: AppearanceIconsOptions - ) -> URL? { - guard let iconName else { return nil } - - return switch iconAppearance { - case .white: - URL(string: ApplicationConfig.shared.whiteAppearanceIconsPath + iconName) - case .colored: - URL(string: ApplicationConfig.shared.coloredAppearanceIconsPath + iconName) - } - } -} - final class AssetReceiveInteractor: AnyCancellableCleaning { weak var presenter: AssetReceiveInteractorOutputProtocol! let chainAsset: ChainAsset + let qrCodeFactory: QRCodeFactoryProtocol let qrCoderFactory: NovaWalletQRCoderFactoryProtocol - let qrCodeCreationOperationFactory: QRCreationOperationFactoryProtocol let metaChainAccountResponse: MetaChainAccountResponse let appearanceFacade: AppearanceFacadeProtocol @@ -33,14 +17,14 @@ final class AssetReceiveInteractor: AnyCancellableCleaning { metaChainAccountResponse: MetaChainAccountResponse, chainAsset: ChainAsset, qrCoderFactory: NovaWalletQRCoderFactoryProtocol, - qrCodeCreationOperationFactory: QRCreationOperationFactoryProtocol, + qrCodeFactory: QRCodeFactoryProtocol, appearanceFacade: AppearanceFacadeProtocol, operationQueue: OperationQueue ) { self.metaChainAccountResponse = metaChainAccountResponse self.chainAsset = chainAsset self.qrCoderFactory = qrCoderFactory - self.qrCodeCreationOperationFactory = qrCodeCreationOperationFactory + self.qrCodeFactory = qrCodeFactory self.appearanceFacade = appearanceFacade self.operationQueue = operationQueue } @@ -60,39 +44,26 @@ final class AssetReceiveInteractor: AnyCancellableCleaning { return } - let iconURL = AssetIconURLFactory.createURL( + let qrLogoType = AssetIconURLFactory.createQRLogoURL( for: chainAsset.asset.icon, iconAppearance: appearanceFacade.selectedIconAppearance ) - let qrCreationOperation = qrCodeCreationOperationFactory.createOperation( - payload: payload, - logoURL: iconURL, - qrSize: size + let logoInfo = QRLogoInfo( + size: .qrLogoSize, + type: qrLogoType ) - qrCreationOperation.completionBlock = { [weak self] in - DispatchQueue.main.async { - guard let self = self, self.currentQRCodeOperation === qrCreationOperation else { - return - } - - self.currentQRCodeOperation = nil - - do { - let qrImage = try qrCreationOperation.extractNoCancellableResultData() - self.presenter.didReceive(qrCodeInfo: .init( - image: qrImage, - encodingData: receiverInfo - )) - } catch { - self.presenter.didReceive(error: .generatingQRCode) - } - } + qrCodeFactory.createQRCode( + with: payload, + logoInfo: logoInfo, + qrSize: size + ) { [weak self] result in + self?.presenter.didReceive(qrCodeInfo: .init( + image: result.image, + encodingData: receiverInfo + )) } - - currentQRCodeOperation = qrCreationOperation - operationQueue.addOperation(qrCreationOperation) } } diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift index 682f38f64e..5157c27a56 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift @@ -13,13 +13,20 @@ struct AssetReceiveViewFactory { publicKey: chainAccount.publicKey ) + let operationQueue = OperationManagerFacade.sharedDefaultQueue + + let qrCodeFactory = QRCodeFactory( + operationFactory: QRCreationOperationFactory(), + operationQueue: operationQueue + ) + let interactor = AssetReceiveInteractor( metaChainAccountResponse: metaChainAccountResponse, chainAsset: chainAsset, qrCoderFactory: qrCoderFactory, - qrCodeCreationOperationFactory: QRCreationOperationFactory(), + qrCodeFactory: qrCodeFactory, appearanceFacade: AppearanceFacade.shared, - operationQueue: OperationManagerFacade.sharedDefaultQueue + operationQueue: operationQueue ) let wireframe = AssetReceiveWireframe() let localizationManager = LocalizationManager.shared diff --git a/novawallet/Modules/AssetReceive/Model/AssetIconURLFactory.swift b/novawallet/Modules/AssetReceive/Model/AssetIconURLFactory.swift new file mode 100644 index 0000000000..6573ee28d4 --- /dev/null +++ b/novawallet/Modules/AssetReceive/Model/AssetIconURLFactory.swift @@ -0,0 +1,37 @@ +import Foundation + +enum AssetIconURLFactory { + static func createURL( + for iconName: String?, + iconAppearance: AppearanceIconsOptions + ) -> URL? { + guard let iconName else { return nil } + + return switch iconAppearance { + case .white: + URL(string: ApplicationConfig.shared.whiteAppearanceIconsPath + iconName) + case .colored: + URL(string: ApplicationConfig.shared.coloredAppearanceIconsPath + iconName) + } + } + + static func createQRLogoURL( + for iconName: String?, + iconAppearance: AppearanceIconsOptions + ) -> QRLogoType? { + guard let iconName else { return nil } + + switch iconAppearance { + case .white: + let path = ApplicationConfig.shared.whiteAppearanceIconsPath + let url = URL(string: path + iconName) + + return .remoteTransparent(url) + case .colored: + let path = ApplicationConfig.shared.coloredAppearanceIconsPath + let url = URL(string: path + iconName) + + return .remoteColored(url) + } + } +} diff --git a/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift b/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift index c03bd4dd02..5993e58a6f 100644 --- a/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift +++ b/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift @@ -3,28 +3,21 @@ import Operation_iOS protocol QRCreationOperationFactoryProtocol { func createOperation( payload: Data, - logoURL: URL?, + logoInfo: QRLogoInfo?, qrSize: CGSize - ) -> BaseOperation + ) -> QRCreationOperation } final class QRCreationOperationFactory: QRCreationOperationFactoryProtocol { - private let logoSize: CGSize - - init(logoSize: CGSize = .init(width: 64, height: 64)) { - self.logoSize = logoSize - } - func createOperation( payload: Data, - logoURL: URL?, + logoInfo: QRLogoInfo?, qrSize: CGSize - ) -> BaseOperation { + ) -> QRCreationOperation { QRCreationOperation( payload: payload, qrSize: qrSize, - logoURL: logoURL, - logoSize: logoSize + logoInfo: logoInfo ) } } diff --git a/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift b/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift index ec5bf721e0..54e1b818dd 100644 --- a/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift +++ b/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift @@ -59,7 +59,7 @@ final class ParitySignerTxQrInteractor { let paylods = try qrPayloadWrapper.targetOperation.extractNoCancellableResultData() return paylods.map { payload in - let operation = QRCreationOperation(payload: payload, qrSize: size, logoURL: nil) + let operation = QRCreationOperation(payload: payload, qrSize: size, logoInfo: nil) return CompoundOperationWrapper(targetOperation: operation) } }.longrunOperation() From d9453d600b70ea888c60388c7426b937c398c56a Mon Sep 17 00:00:00 2001 From: svojsu Date: Wed, 30 Oct 2024 18:57:30 +0200 Subject: [PATCH 06/12] make view support animated update --- .../Common/QRCreation/QRCodeFactory.swift | 13 +++++- .../QRCreation/QRCreationOperation.swift | 19 +++++--- .../Common/QRCreation/QRDisplayView.swift | 43 ++++++++++++++++--- .../AssetReceive/AssetReceiveInteractor.swift | 2 +- .../AssetReceive/AssetReceivePresenter.swift | 4 +- .../AssetReceive/AssetReceiveProtocols.swift | 2 +- .../AssetReceiveViewController.swift | 4 +- .../AssetReceive/Model/QRCodeInfo.swift | 2 +- .../ParitySignerTxQrViewController.swift | 2 +- 9 files changed, 70 insertions(+), 21 deletions(-) diff --git a/novawallet/Common/QRCreation/QRCodeFactory.swift b/novawallet/Common/QRCreation/QRCodeFactory.swift index 39a6bf194c..a624406dcf 100644 --- a/novawallet/Common/QRCreation/QRCodeFactory.swift +++ b/novawallet/Common/QRCreation/QRCodeFactory.swift @@ -122,10 +122,19 @@ private extension QRCodeFactory { var updatedLogoInfo: QRLogoInfo? = logoInfo - if case let .success(cacheResult) = result, let image = cacheResult.image { + if + case let .success(cacheResult) = result, + let image = cacheResult.image, + let type = logoInfo.type { + let localType: QRLogoType? = switch type { + case let .remoteColored(url): .localColored(image) + case let .remoteTransparent(url): .localTransparent(image) + default: nil + } + updatedLogoInfo = QRLogoInfo( size: logoInfo.size, - type: .local(image) + type: localType ) } diff --git a/novawallet/Common/QRCreation/QRCreationOperation.swift b/novawallet/Common/QRCreation/QRCreationOperation.swift index c3aacb5a49..28c7167ab7 100644 --- a/novawallet/Common/QRCreation/QRCreationOperation.swift +++ b/novawallet/Common/QRCreation/QRCreationOperation.swift @@ -13,7 +13,8 @@ enum QRCreationOperationError: Error { enum QRLogoType { case remoteColored(URL?) case remoteTransparent(URL?) - case local(UIImage) + case localColored(UIImage) + case localTransparent(UIImage) var url: URL? { switch self { @@ -99,14 +100,22 @@ final class QRCreationOperation: BaseOperation { return } - if case let .local(image) = logoInfo.type { - qrDoc.logoTemplate = QRCode.LogoTemplate.SquareCenter( + switch logoInfo.type { + case let .localColored(image): + qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter( image: image.cgImage!, inset: 0 ) try qrCreateImageClosure() - } else { + case let .localTransparent(image): + qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter( + image: image.cgImage!, + inset: 20 + ) + + try qrCreateImageClosure() + default: try downloadImage( using: logoInfo, scale: scale @@ -116,7 +125,7 @@ final class QRCreationOperation: BaseOperation { default: 0 } - qrDoc.logoTemplate = QRCode.LogoTemplate.SquareCenter( + qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter( image: logoImage, inset: inset ) diff --git a/novawallet/Common/QRCreation/QRDisplayView.swift b/novawallet/Common/QRCreation/QRDisplayView.swift index b7f16725f5..388ea256ea 100644 --- a/novawallet/Common/QRCreation/QRDisplayView.swift +++ b/novawallet/Common/QRCreation/QRDisplayView.swift @@ -9,12 +9,19 @@ final class QRDisplayView: UIView { return view }() - let imageView = UIImageView() + let noLogoQRImageView = UIImageView() + let fullQRImageView: UIImageView = .create { view in + view.alpha = 0 + } + + var viewModel: QRCodeFactory.Result? var contentInsets: CGFloat = 8.0 { didSet { - imageView.snp.updateConstraints { make in - make.edges.equalToSuperview().inset(contentInsets) + [noLogoQRImageView, fullQRImageView].forEach { + $0.snp.updateConstraints { make in + make.edges.equalToSuperview().inset(contentInsets) + } } } } @@ -42,15 +49,39 @@ final class QRDisplayView: UIView { fatalError("init(coder:) has not been implemented") } + func bind(viewModel: QRCodeFactory.Result) { + switch viewModel { + case let .noLogo(image): + noLogoQRImageView.image = image + case let .full(image) where self.viewModel != nil && self.viewModel != viewModel: + fullQRImageView.image = image + + UIView.animate(withDuration: 0.3) { + self.fullQRImageView.alpha = 1 + } + case let .full(image): + fullQRImageView.image = image + fullQRImageView.alpha = 1 + } + + self.viewModel = viewModel + } + private func setupLayout() { addSubview(backgroundView) backgroundView.snp.makeConstraints { make in make.edges.equalToSuperview() } - addSubview(imageView) - imageView.snp.makeConstraints { make in - make.edges.equalToSuperview().inset(contentInsets) + [ + noLogoQRImageView, + fullQRImageView + ] + .forEach { view in + addSubview(view) + view.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(contentInsets) + } } } } diff --git a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift index 6f9fbc0cb2..41ee9cdd04 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift @@ -60,7 +60,7 @@ final class AssetReceiveInteractor: AnyCancellableCleaning { qrSize: size ) { [weak self] result in self?.presenter.didReceive(qrCodeInfo: .init( - image: result.image, + result: result, encodingData: receiverInfo )) } diff --git a/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift b/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift index 78a5fe67f2..9cb8eb8085 100644 --- a/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift +++ b/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift @@ -77,7 +77,7 @@ extension AssetReceivePresenter: AssetReceivePresenterProtocol { } let sharingItems = accountShareFactory.createSources( for: qrCodeInfo.encodingData, - qrImage: qrCodeInfo.image + qrImage: qrCodeInfo.result.image ) wireframe.share( @@ -126,7 +126,7 @@ extension AssetReceivePresenter: AssetReceiveInteractorOutputProtocol { func didReceive(qrCodeInfo: QRCodeInfo) { self.qrCodeInfo = qrCodeInfo - view?.didReceive(qrImage: qrCodeInfo.image) + view?.didReceive(qrResult: qrCodeInfo.result) } func didReceive(error: AssetReceiveInteractorError) { diff --git a/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift b/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift index cf24422c54..20ee3557a1 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift @@ -7,7 +7,7 @@ protocol AssetReceiveViewProtocol: ControllerBackedProtocol { networkName: String, token: String ) - func didReceive(qrImage: UIImage) + func didReceive(qrResult: QRCodeFactory.Result) } protocol AssetReceivePresenterProtocol: AnyObject { diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift index ed0e006dba..75814a188d 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift @@ -119,8 +119,8 @@ extension AssetReceiveViewController: AssetReceiveViewProtocol { rootView.accountAddressView.addressLabel.text = addressViewModel.address } - func didReceive(qrImage: UIImage) { - rootView.qrView.imageView.image = qrImage + func didReceive(qrResult: QRCodeFactory.Result) { + rootView.qrView.bind(viewModel: qrResult) } func didReceive(networkViewModel: NetworkViewModel) { diff --git a/novawallet/Modules/AssetReceive/Model/QRCodeInfo.swift b/novawallet/Modules/AssetReceive/Model/QRCodeInfo.swift index 605a92e2b9..84893a322e 100644 --- a/novawallet/Modules/AssetReceive/Model/QRCodeInfo.swift +++ b/novawallet/Modules/AssetReceive/Model/QRCodeInfo.swift @@ -1,6 +1,6 @@ import UIKit struct QRCodeInfo { - let image: UIImage + let result: QRCodeFactory.Result let encodingData: AssetReceiveInfo } diff --git a/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrViewController.swift b/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrViewController.swift index 6159edcc79..0cb660c733 100644 --- a/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrViewController.swift +++ b/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrViewController.swift @@ -116,7 +116,7 @@ extension ParitySignerTxQrViewController: ParitySignerTxQrViewProtocol { } func didReceiveCode(viewModel: QRImageViewModel) { - rootView.qrView.imageView.bindQr(viewModel: viewModel) + rootView.qrView.noLogoQRImageView.bindQr(viewModel: viewModel) } func didReceiveExpiration(viewModel: ExpirationTimeViewModel) { From 4cc331181c559f9867f037be69adfb8cfba41b64 Mon Sep 17 00:00:00 2001 From: svojsu Date: Thu, 31 Oct 2024 23:40:18 +0200 Subject: [PATCH 07/12] review fixes --- novawallet.xcodeproj/project.pbxproj | 52 ++- .../Logo/QRCodeWithLogoFactory.swift | 328 ++++++++++++++++++ .../Common/QRCreation/Logo/QRLogoInfo.swift | 38 ++ .../Common/QRCreation/Logo/QRLogoType.swift | 33 ++ .../Logo/QRWithLogoCreationOperation.swift | 82 +++++ .../NoLogo/QRCreationOperation.swift | 61 ++++ .../NoLogo/QRCreationOperationFactory.swift | 11 + .../Common/QRCreation/QRCodeFactory.swift | 203 ----------- .../QRCreation/QRCreationOperation.swift | 195 ----------- .../Common/QRCreation/QRDisplayView.swift | 4 +- .../AssetReceive/AssetReceiveInteractor.swift | 17 +- .../AssetReceive/AssetReceivePresenter.swift | 34 +- .../AssetReceive/AssetReceiveProtocols.swift | 2 +- .../AssetReceiveViewController.swift | 2 +- .../AssetReceiveViewFactory.swift | 6 +- .../AssetReceive/Model/QRCodeInfo.swift | 2 +- .../QRCreationOperationFactory.swift | 23 -- .../ParitySignerTxQrInteractor.swift | 2 +- 18 files changed, 636 insertions(+), 459 deletions(-) create mode 100644 novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift create mode 100644 novawallet/Common/QRCreation/Logo/QRLogoInfo.swift create mode 100644 novawallet/Common/QRCreation/Logo/QRLogoType.swift create mode 100644 novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift create mode 100644 novawallet/Common/QRCreation/NoLogo/QRCreationOperation.swift create mode 100644 novawallet/Common/QRCreation/NoLogo/QRCreationOperationFactory.swift delete mode 100644 novawallet/Common/QRCreation/QRCodeFactory.swift delete mode 100644 novawallet/Common/QRCreation/QRCreationOperation.swift delete mode 100644 novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 8c81d535ae..bcc5d0b0b0 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -1070,8 +1070,12 @@ 2DAF539E2C1B2AA00076B4B6 /* NodePingOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAF539D2C1B2AA00076B4B6 /* NodePingOperationFactory.swift */; }; 2DB18C8F2C36AB1C00A93C75 /* LightChainsFetchFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB18C8E2C36AB1C00A93C75 /* LightChainsFetchFactory.swift */; }; 2DB18C922C36D1BC00A93C75 /* PreConfiguredChainFetchFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB18C912C36D1BC00A93C75 /* PreConfiguredChainFetchFactory.swift */; }; - 2DBB28852CD2811200DFA0AD /* QRCodeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28842CD2811200DFA0AD /* QRCodeFactory.swift */; }; + 2DBB28852CD2811200DFA0AD /* QRCodeWithLogoFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28842CD2811200DFA0AD /* QRCodeWithLogoFactory.swift */; }; 2DBB28872CD2934500DFA0AD /* AssetIconURLFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28862CD2934500DFA0AD /* AssetIconURLFactory.swift */; }; + 2DBB28892CD42ACB00DFA0AD /* QRCreationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28882CD42ACB00DFA0AD /* QRCreationOperation.swift */; }; + 2DBB288D2CD42C5200DFA0AD /* QRCreationOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB288C2CD42C5200DFA0AD /* QRCreationOperationFactory.swift */; }; + 2DBB288F2CD42E8D00DFA0AD /* QRLogoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB288E2CD42E8D00DFA0AD /* QRLogoType.swift */; }; + 2DBB28912CD42ED800DFA0AD /* QRLogoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28902CD42ED800DFA0AD /* QRLogoInfo.swift */; }; 2DC075E32BF24AB400868563 /* CheckBoxIconDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC075E22BF24AB400868563 /* CheckBoxIconDetailsView.swift */; }; 2DC075E92BF4B97900868563 /* BackupAttentionTableTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC075E82BF4B97900868563 /* BackupAttentionTableTitleView.swift */; }; 2DCC875B2C5820BA0028C3CA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849013D424A927E2008F705E /* Logger.swift */; }; @@ -3501,7 +3505,7 @@ 84BB3CF3267D24B000676FFE /* UIStackView+Manage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB3CF2267D24B000676FFE /* UIStackView+Manage.swift */; }; 84BB3CF8267D276D00676FFE /* CrowdloanTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB3CF7267D276D00676FFE /* CrowdloanTableViewCell.swift */; }; 84BB3D00267D364D00676FFE /* CompoundOperationWrapper+Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB3CFF267D364D00676FFE /* CompoundOperationWrapper+Dependency.swift */; }; - 84BC703F289DBE6C008A9758 /* QRCreationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC703E289DBE6C008A9758 /* QRCreationOperation.swift */; }; + 84BC703F289DBE6C008A9758 /* QRWithLogoCreationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC703E289DBE6C008A9758 /* QRWithLogoCreationOperation.swift */; }; 84BC7041289DBF62008A9758 /* QRDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC7040289DBF62008A9758 /* QRDisplayView.swift */; }; 84BC7043289EEF85008A9758 /* NoSigningSupportWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC7042289EEF85008A9758 /* NoSigningSupportWrapper.swift */; }; 84BC7045289EFF44008A9758 /* TransactionDisplayCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC7044289EFF44008A9758 /* TransactionDisplayCode.swift */; }; @@ -4313,7 +4317,6 @@ 88E74E8829538BF8008031A3 /* AssetReceiveInteractorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E74E8729538BF8008031A3 /* AssetReceiveInteractorError.swift */; }; 88E74E8A29538C1F008031A3 /* NovaAccountShareFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E74E8929538C1F008031A3 /* NovaAccountShareFactoryProtocol.swift */; }; 88E74E8C29538C36008031A3 /* QRCodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E74E8B29538C36008031A3 /* QRCodeInfo.swift */; }; - 88E74E8E29539E18008031A3 /* QRCreationOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E74E8D29539E18008031A3 /* QRCreationOperationFactory.swift */; }; 88E8CF5E28E3789600C90112 /* CrowdloanEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E8CF5D28E3789600C90112 /* CrowdloanEmptyView.swift */; }; 88F19DDE28D8D0A100F6E459 /* Either.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F19DDD28D8D0A100F6E459 /* Either.swift */; }; 88F19DE028D8D0F600F6E459 /* LoadableViewModelState+Addition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88F19DDF28D8D0F600F6E459 /* LoadableViewModelState+Addition.swift */; }; @@ -6249,8 +6252,12 @@ 2DAF539D2C1B2AA00076B4B6 /* NodePingOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePingOperationFactory.swift; sourceTree = ""; }; 2DB18C8E2C36AB1C00A93C75 /* LightChainsFetchFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightChainsFetchFactory.swift; sourceTree = ""; }; 2DB18C912C36D1BC00A93C75 /* PreConfiguredChainFetchFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreConfiguredChainFetchFactory.swift; sourceTree = ""; }; - 2DBB28842CD2811200DFA0AD /* QRCodeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeFactory.swift; sourceTree = ""; }; + 2DBB28842CD2811200DFA0AD /* QRCodeWithLogoFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeWithLogoFactory.swift; sourceTree = ""; }; 2DBB28862CD2934500DFA0AD /* AssetIconURLFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetIconURLFactory.swift; sourceTree = ""; }; + 2DBB28882CD42ACB00DFA0AD /* QRCreationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCreationOperation.swift; sourceTree = ""; }; + 2DBB288C2CD42C5200DFA0AD /* QRCreationOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCreationOperationFactory.swift; sourceTree = ""; }; + 2DBB288E2CD42E8D00DFA0AD /* QRLogoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRLogoType.swift; sourceTree = ""; }; + 2DBB28902CD42ED800DFA0AD /* QRLogoInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRLogoInfo.swift; sourceTree = ""; }; 2DC075E22BF24AB400868563 /* CheckBoxIconDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBoxIconDetailsView.swift; sourceTree = ""; }; 2DC075E82BF4B97900868563 /* BackupAttentionTableTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAttentionTableTitleView.swift; sourceTree = ""; }; 2DC70DB97D2E9350022A899B /* TokensManagePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManagePresenter.swift; sourceTree = ""; }; @@ -8627,7 +8634,7 @@ 84BB3CF2267D24B000676FFE /* UIStackView+Manage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Manage.swift"; sourceTree = ""; }; 84BB3CF7267D276D00676FFE /* CrowdloanTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanTableViewCell.swift; sourceTree = ""; }; 84BB3CFF267D364D00676FFE /* CompoundOperationWrapper+Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompoundOperationWrapper+Dependency.swift"; sourceTree = ""; }; - 84BC703E289DBE6C008A9758 /* QRCreationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCreationOperation.swift; sourceTree = ""; }; + 84BC703E289DBE6C008A9758 /* QRWithLogoCreationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRWithLogoCreationOperation.swift; sourceTree = ""; }; 84BC7040289DBF62008A9758 /* QRDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRDisplayView.swift; sourceTree = ""; }; 84BC7042289EEF85008A9758 /* NoSigningSupportWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoSigningSupportWrapper.swift; sourceTree = ""; }; 84BC7044289EFF44008A9758 /* TransactionDisplayCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDisplayCode.swift; sourceTree = ""; }; @@ -9451,7 +9458,6 @@ 88E74E8729538BF8008031A3 /* AssetReceiveInteractorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetReceiveInteractorError.swift; sourceTree = ""; }; 88E74E8929538C1F008031A3 /* NovaAccountShareFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NovaAccountShareFactoryProtocol.swift; sourceTree = ""; }; 88E74E8B29538C36008031A3 /* QRCodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeInfo.swift; sourceTree = ""; }; - 88E74E8D29539E18008031A3 /* QRCreationOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCreationOperationFactory.swift; sourceTree = ""; }; 88E8CF5D28E3789600C90112 /* CrowdloanEmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrowdloanEmptyView.swift; sourceTree = ""; }; 88F19DDD28D8D0A100F6E459 /* Either.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Either.swift; sourceTree = ""; }; 88F19DDF28D8D0F600F6E459 /* LoadableViewModelState+Addition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoadableViewModelState+Addition.swift"; sourceTree = ""; }; @@ -12908,6 +12914,26 @@ path = View; sourceTree = ""; }; + 2DBB288A2CD42B5100DFA0AD /* Logo */ = { + isa = PBXGroup; + children = ( + 84BC703E289DBE6C008A9758 /* QRWithLogoCreationOperation.swift */, + 2DBB28842CD2811200DFA0AD /* QRCodeWithLogoFactory.swift */, + 2DBB288E2CD42E8D00DFA0AD /* QRLogoType.swift */, + 2DBB28902CD42ED800DFA0AD /* QRLogoInfo.swift */, + ); + path = Logo; + sourceTree = ""; + }; + 2DBB288B2CD42B6800DFA0AD /* NoLogo */ = { + isa = PBXGroup; + children = ( + 2DBB28882CD42ACB00DFA0AD /* QRCreationOperation.swift */, + 2DBB288C2CD42C5200DFA0AD /* QRCreationOperationFactory.swift */, + ); + path = NoLogo; + sourceTree = ""; + }; 2DC075E12BF249F000868563 /* View */ = { isa = PBXGroup; children = ( @@ -19796,8 +19822,8 @@ 84BC703D289DBE48008A9758 /* QRCreation */ = { isa = PBXGroup; children = ( - 84BC703E289DBE6C008A9758 /* QRCreationOperation.swift */, - 2DBB28842CD2811200DFA0AD /* QRCodeFactory.swift */, + 2DBB288A2CD42B5100DFA0AD /* Logo */, + 2DBB288B2CD42B6800DFA0AD /* NoLogo */, 84BC7040289DBF62008A9758 /* QRDisplayView.swift */, 2D442CF22CD103ED00A0380F /* BarcodeCreationError.swift */, ); @@ -23356,7 +23382,6 @@ 73DE88ED69EF6E4F4F6612D8 /* AssetReceiveViewLayout.swift */, 1C5C524FD0E3E7E5113F325D /* AssetReceiveViewFactory.swift */, 88E74E8929538C1F008031A3 /* NovaAccountShareFactoryProtocol.swift */, - 88E74E8D29539E18008031A3 /* QRCreationOperationFactory.swift */, ); path = AssetReceive; sourceTree = ""; @@ -25684,7 +25709,7 @@ 0C3205E82A898195002EB914 /* EvmValidationErrorPresentable.swift in Sources */, 84CE69E82566750D00559427 /* ByteLengthProcessor.swift in Sources */, 8499FEDA27BFDB8C00712589 /* NFTStreamableSource.swift in Sources */, - 2DBB28852CD2811200DFA0AD /* QRCodeFactory.swift in Sources */, + 2DBB28852CD2811200DFA0AD /* QRCodeWithLogoFactory.swift in Sources */, 0CE360212C33F398006A6CE4 /* GraphModel+Dijkstra.swift in Sources */, 849976BE27B269A400B14A6C /* DAppTransports.swift in Sources */, 842876A724AE049B00D91AD8 /* SelectionListProtocols.swift in Sources */, @@ -25905,10 +25930,12 @@ 8418167528251BBC0007684A /* StorageListSyncResult.swift in Sources */, 84DD261929ACA9880032A598 /* BagList+CodingPath.swift in Sources */, 8465DA3F298EEC6C00C7CFF1 /* GovernanceAddDelegationTracksInteractor.swift in Sources */, + 2DBB288D2CD42C5200DFA0AD /* QRCreationOperationFactory.swift in Sources */, 845B823429C8FF0700D187CB /* EtherscanNativeOperationFactory.swift in Sources */, 88D997B028ABC8C0006135A5 /* YourContributionsTableViewCell.swift in Sources */, 849ABE5B2627739400011A2A /* ListReducing.swift in Sources */, 0C7CF4CC2BD374930015DD45 /* CloudBackupFileManaging.swift in Sources */, + 2DBB28912CD42ED800DFA0AD /* QRLogoInfo.swift in Sources */, 88C5F07C297EE79C001CCADE /* Release.swift in Sources */, 845B89222959620000EE25B0 /* SecurityLayerPresenter.swift in Sources */, 849E07F4284A04F400DE0440 /* ParaStkAccountSubscribeHandlingFactory.swift in Sources */, @@ -26835,7 +26862,7 @@ 84E25BE427E5F71900290BF1 /* AccountFieldStateViewModel.swift in Sources */, 84452F4E25D5BB1C00F47EC5 /* RuntimeCoderFactory.swift in Sources */, 773A375C2B3B5901006AC4AA /* ProxyMessageSheetPresenter.swift in Sources */, - 84BC703F289DBE6C008A9758 /* QRCreationOperation.swift in Sources */, + 84BC703F289DBE6C008A9758 /* QRWithLogoCreationOperation.swift in Sources */, 84452A6025D037AE00F47EC5 /* ChainStorage+Decodable.swift in Sources */, 8849AD6E29C4690F00F4F7FF /* String+CheckLength.swift in Sources */, 847C963525534E41002D288F /* UIFactory.swift in Sources */, @@ -27443,6 +27470,7 @@ 7725062C2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift in Sources */, 84466B3528B6731B00FA1E0D /* LedgerSigningWrapper.swift in Sources */, 0C38B4FB2B7A5DB500882A8B /* ExtrinsicProcessor+HydraSwapMatching.swift in Sources */, + 2DBB288F2CD42E8D00DFA0AD /* QRLogoType.swift in Sources */, F4223ED127329767003D8E4E /* AcalaTransferRequest.swift in Sources */, 8473F4B8282BFFF8007CC55A /* StakingRelaychainInteractor+Subscription.swift in Sources */, 841E2E5027381B2A00F250C1 /* AccountInfoSubscriptionHandlingFactory.swift in Sources */, @@ -27464,6 +27492,7 @@ 0CEB6B3A2CA4597600609DC2 /* Multilocation+AccountId.swift in Sources */, 84468A0F2867ADA400BCBE00 /* XcmTransferRequest.swift in Sources */, 848DAF022822AA1100D56F55 /* ParachainStakingCollatorService.swift in Sources */, + 2DBB28892CD42ACB00DFA0AD /* QRCreationOperation.swift in Sources */, F4FDA0FD26A57860003D753B /* EraCountdown.swift in Sources */, 844CB56C26F9C5C900396E13 /* WalletLocalSubscriptionHandler.swift in Sources */, AEA0C8B6267BABCC00F9666F /* SelectedValidatorListViewModel.swift in Sources */, @@ -28517,7 +28546,6 @@ 89724EA9F732D0C967253597 /* ReferendumOnChainVotersViewFactory.swift in Sources */, 1F496969FEE3E160BABDAC66 /* ReferendumVoteSetupProtocols.swift in Sources */, F1BED07F67119E1BD052952A /* ReferendumVoteSetupWireframe.swift in Sources */, - 88E74E8E29539E18008031A3 /* QRCreationOperationFactory.swift in Sources */, 04D86D5341406305E60F6D18 /* ReferendumVoteSetupInteractor.swift in Sources */, 2CEFF4C2574F0AABE0E9BF89 /* BaseReferendumVoteSetupViewController.swift in Sources */, 0C59E8CF2AA5D744001E11F3 /* PooledBalanceUpdatingState.swift in Sources */, diff --git a/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift b/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift new file mode 100644 index 0000000000..3f3d0376c5 --- /dev/null +++ b/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift @@ -0,0 +1,328 @@ +import Foundation +import Kingfisher +import Operation_iOS + +enum QRCodeFactoryError: Error { + case logoDownloadError + case logoRetrievingError +} + +protocol QRCodeWithLogoFactoryProtocol { + func createQRCode( + with payload: Data, + logoInfo: QRLogoInfo?, + qrSize: CGSize, + completion: @escaping (Result) -> Void + ) +} + +final class QRCodeWithLogoFactory { + enum QRCreationResult: Equatable { + case full(UIImage) + case noLogo(UIImage) + + var image: UIImage { + switch self { + case let .full(image), let .noLogo(image): + return image + } + } + } + + let imageManager: KingfisherManager + let operationQueue: OperationQueue + let logger: LoggerProtocol + + init( + imageManager: KingfisherManager = KingfisherManager.shared, + operationQueue: OperationQueue, + logger: LoggerProtocol + ) { + self.imageManager = imageManager + self.operationQueue = operationQueue + self.logger = logger + } +} + +// MARK: QRCodeFactoryProtocol + +extension QRCodeWithLogoFactory: QRCodeWithLogoFactoryProtocol { + func createQRCode( + with payload: Data, + logoInfo: QRLogoInfo?, + qrSize: CGSize, + completion: @escaping (Result) -> Void + ) { + let wrapper = createQRWrapper( + with: payload, + logoInfo: logoInfo, + qrSize: qrSize + ) + + novawallet.execute( + wrapper: wrapper, + inOperationQueue: operationQueue, + runningCallbackIn: .main, + callbackClosure: completion + ) + } +} + +// MARK: Private + +private extension QRCodeWithLogoFactory { + struct QRWrapperResult { + let wrapper: CompoundOperationWrapper + let remoteLogo: Bool + } + + func checkCacheOperation( + in cache: ImageCache, + using logoInfo: QRLogoInfo? + ) -> ClosureOperation { + ClosureOperation { + guard + let logoInfo, + let cacheKey = logoInfo.type?.cacheKey + else { + return false + } + + let cachedType = cache.imageCachedType(forKey: cacheKey) + + return cachedType.cached + } + } + + func createQRWrapper( + with payload: Data, + logoInfo: QRLogoInfo?, + qrSize: CGSize + ) -> CompoundOperationWrapper { + let cache = imageManager.cache + + let checkCacheOperation = checkCacheOperation(in: cache, using: logoInfo) + + var remoteLogo = false + + let wrapper: CompoundOperationWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationManager: OperationManager(operationQueue: operationQueue) + ) { [weak self] in + guard let self, let logoInfo else { + return .createWithError(BaseOperationError.parentOperationCancelled) + } + + let cached = try checkCacheOperation.extractNoCancellableResultData() + let usesCache: Bool = logoInfo.type?.cacheKey != nil + + return if !cached, usesCache { + createDownloadLogoQRWrapper( + payload: payload, + downloader: imageManager.downloader, + logoInfo: logoInfo, + qrSize: qrSize + ) + } else if let cacheKey = logoInfo.type?.cacheKey { + createCachedLogoQRWrapper( + using: cache, + cacheKey: cacheKey, + payload: payload, + logoInfo: logoInfo, + qrSize: qrSize + ) + } else if let image = logoInfo.type?.image { + createQRResultWrapper( + using: .createWithResult(image), + payload: payload, + logoInfo: logoInfo, + qrSize: qrSize + ) + } else { + createQRResultWrapper( + using: nil, + payload: payload, + logoInfo: logoInfo, + qrSize: qrSize + ) + } + } + + wrapper.addDependency(operations: [checkCacheOperation]) + + return wrapper.insertingHead(operations: [checkCacheOperation]) + } + + func createDownloadLogoQRWrapper( + payload: Data, + downloader: ImageDownloader, + logoInfo: QRLogoInfo, + qrSize: CGSize + ) -> CompoundOperationWrapper { + let logoOperation = downloadImageOperation( + using: logoInfo, + downloader: downloader + ) + + return createQRResultWrapper( + using: logoOperation, + payload: payload, + logoInfo: logoInfo, + qrSize: qrSize + ) + } + + func createCachedLogoQRWrapper( + using cache: ImageCache, + cacheKey: String, + payload: Data, + logoInfo: QRLogoInfo, + qrSize: CGSize + ) -> CompoundOperationWrapper { + let logoOperation = retrieveImageOperation( + using: cache, + cacheKey: cacheKey + ) + + return createQRResultWrapper( + using: logoOperation, + payload: payload, + logoInfo: logoInfo, + qrSize: qrSize + ) + } + + func createQRResultWrapper( + using logoOperation: BaseOperation?, + payload: Data, + logoInfo: QRLogoInfo, + qrSize: CGSize + ) -> CompoundOperationWrapper { + var qrImageWrapper: CompoundOperationWrapper + qrImageWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationManager: OperationManager(operationQueue: operationQueue) + ) { [weak self] in + guard let self else { + return .createWithError(BaseOperationError.parentOperationCancelled) + } + + var updatedLogoInfo: QRLogoInfo? = logoInfo + + if let logoOperation { + do { + let logoImage = try logoOperation.extractNoCancellableResultData() + updatedLogoInfo = logoInfo.byChangingToLocal(logoImage) + } catch { + logger.error(error.localizedDescription) + } + } + + let qrCodeOperation = QRWithLogoCreationOperation( + payload: payload, + qrSize: qrSize, + logoInfo: updatedLogoInfo + ) + + return CompoundOperationWrapper(targetOperation: qrCodeOperation) + } + + if let logoOperation { + qrImageWrapper.addDependency(operations: [logoOperation]) + qrImageWrapper = qrImageWrapper.insertingHead(operations: [logoOperation]) + } + + let resultMappingWrapper: CompoundOperationWrapper + resultMappingWrapper = OperationCombiningService.compoundNonOptionalWrapper( + operationManager: OperationManager(operationQueue: operationQueue) + ) { + let qrImage = try qrImageWrapper.targetOperation.extractNoCancellableResultData() + + let result: QRCreationResult = if logoInfo.type != nil { + .full(qrImage) + } else { + .noLogo(qrImage) + } + + return .createWithResult(result) + } + + resultMappingWrapper.addDependency(wrapper: qrImageWrapper) + + return resultMappingWrapper.insertingHead(operations: qrImageWrapper.allOperations) + } + + func downloadImageOperation( + using logoInfo: QRLogoInfo, + downloader: ImageDownloader + ) -> AsyncClosureOperation { + AsyncClosureOperation { resultClosure in + let scale = UIScreen.main.scale + + let scaledSize = CGSize( + width: logoInfo.size.width * scale, + height: logoInfo.size.height * scale + ) + + guard let url = logoInfo.url else { + resultClosure(.failure(QRCodeFactoryError.logoDownloadError)) + + return + } + + let options: KingfisherOptionsInfo = [ + .processor(ResizingImageProcessor(referenceSize: scaledSize)), + .processor(SVGImageProcessor()) + ] + + downloader.downloadImage(with: url, options: options) { result in + var resultImage: UIImage + + switch result { + case let .success(imageResult) where imageResult.image.cgImage != nil: + resultImage = imageResult.image + default: + resultClosure(.failure(QRCodeFactoryError.logoDownloadError)) + + return + } + + let sizeBeforeProcessing = resultImage.size + + if case .remoteTransparent = logoInfo.type { + resultImage = resultImage.redrawWithBackground( + color: R.color.colorTextPrimaryOnWhite()!, + shape: .circle + ) + } + + if let cacheKey = logoInfo.type?.cacheKey { + KingfisherManager.shared.cache.store( + resultImage, + forKey: cacheKey, + options: KingfisherParsedOptionsInfo(nil) + ) + } + + resultClosure(.success(resultImage)) + } + } + } + + func retrieveImageOperation( + using cache: ImageCache, + cacheKey: String + ) -> AsyncClosureOperation { + AsyncClosureOperation { resultClosure in + cache.retrieveImage(forKey: cacheKey) { [weak self] result in + guard let self else { return } + + if + case let .success(cacheResult) = result, + let image = cacheResult.image { + resultClosure(.success(image)) + } else { + resultClosure(.failure(QRCodeFactoryError.logoRetrievingError)) + } + } + } + } +} diff --git a/novawallet/Common/QRCreation/Logo/QRLogoInfo.swift b/novawallet/Common/QRCreation/Logo/QRLogoInfo.swift new file mode 100644 index 0000000000..72f3305a60 --- /dev/null +++ b/novawallet/Common/QRCreation/Logo/QRLogoInfo.swift @@ -0,0 +1,38 @@ +import Foundation +import UIKit + +struct QRLogoInfo { + let size: CGSize + let type: QRLogoType? + + var url: URL? { + type?.url + } + + func byChangingToLocal(_ image: UIImage) -> QRLogoInfo? { + switch type { + case .remoteColored: + QRLogoInfo( + size: size, + type: .localColored(image) + ) + case .remoteTransparent: + QRLogoInfo( + size: size, + type: .localTransparent(image) + ) + default: + QRLogoInfo( + size: size, + type: nil + ) + } + } + + func withNoLogo() -> QRLogoInfo { + QRLogoInfo( + size: size, + type: nil + ) + } +} diff --git a/novawallet/Common/QRCreation/Logo/QRLogoType.swift b/novawallet/Common/QRCreation/Logo/QRLogoType.swift new file mode 100644 index 0000000000..24e08b6cc5 --- /dev/null +++ b/novawallet/Common/QRCreation/Logo/QRLogoType.swift @@ -0,0 +1,33 @@ +import Foundation +import UIKit + +enum QRLogoType { + case remoteColored(URL?) + case remoteTransparent(URL?) + case localColored(UIImage) + case localTransparent(UIImage) + + var url: URL? { + switch self { + case let .remoteColored(url), let .remoteTransparent(url): + return url + default: + return nil + } + } + + var cacheKey: String? { + guard let url else { return nil } + + return url.absoluteString + String(describing: self) + } + + var image: UIImage? { + switch self { + case let .localColored(image), let .localTransparent(image): + return image + default: + return nil + } + } +} diff --git a/novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift b/novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift new file mode 100644 index 0000000000..a71a49669f --- /dev/null +++ b/novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift @@ -0,0 +1,82 @@ +import Foundation +import CoreImage +import Operation_iOS +import Kingfisher +import QRCode + +final class QRWithLogoCreationOperation: BaseOperation { + let payloadClosure: () throws -> Data + let qrSize: CGSize + let logoInfo: QRLogoInfo? + + init( + payload: Data, + qrSize: CGSize, + logoInfo: QRLogoInfo? = nil + ) { + payloadClosure = { payload } + self.qrSize = qrSize + self.logoInfo = logoInfo + + super.init() + } + + init( + qrSize: CGSize, + logoInfo: QRLogoInfo? = nil, + payloadClosure: @escaping () throws -> Data + ) { + self.qrSize = qrSize + self.logoInfo = logoInfo + self.payloadClosure = payloadClosure + } + + override func performAsync(_ callback: @escaping (Result) -> Void) throws { + let data = try payloadClosure() + let qrDoc = QRCode.Document(data: data) + qrDoc.design.backgroundColor(UIColor.white.cgColor) + qrDoc.design.shape.eye = QRCode.EyeShape.Squircle() + qrDoc.design.shape.onPixels = QRCode.PixelShape.Circle(insetFraction: 0.2) + qrDoc.design.style.onPixels = QRCode.FillStyle.Solid(UIColor.black.cgColor) + qrDoc.design.shape.offPixels = nil + qrDoc.design.style.offPixels = nil + + let qrCreateImageClosure: () throws -> Void = { [weak self] in + guard let self else { return } + + let scale = UIScreen.main.scale + + let scaledSize = CGSize( + width: qrSize.width * scale, + height: qrSize.height * scale + ) + + guard let cgImage = qrDoc.cgImage(scaledSize) else { + throw BarcodeCreationError.bitmapImageCreationFailed + } + + callback(.success(UIImage(cgImage: cgImage))) + } + + switch logoInfo?.type { + case let .localColored(image): + qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter( + image: image.cgImage!, + inset: 0 + ) + case let .localTransparent(image): + qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter( + image: image.cgImage!, + inset: 20 + ) + default: + break + } + + try qrCreateImageClosure() + } +} + +extension CGSize { + static let qrLogoSize: CGSize = .init(width: 80, height: 80) +} diff --git a/novawallet/Common/QRCreation/NoLogo/QRCreationOperation.swift b/novawallet/Common/QRCreation/NoLogo/QRCreationOperation.swift new file mode 100644 index 0000000000..4dca962da0 --- /dev/null +++ b/novawallet/Common/QRCreation/NoLogo/QRCreationOperation.swift @@ -0,0 +1,61 @@ +import Foundation +import CoreImage +import Operation_iOS + +enum QRCreationOperationError: Error { + case generatorUnavailable + case generatedImageInvalid + case bitmapImageCreationFailed +} + +final class QRCreationOperation: BaseOperation { + let payloadClosure: () throws -> Data + let qrSize: CGSize + + init(payload: Data, qrSize: CGSize) { + payloadClosure = { payload } + self.qrSize = qrSize + + super.init() + } + + init(qrSize: CGSize, payloadClosure: @escaping () throws -> Data) { + self.qrSize = qrSize + self.payloadClosure = payloadClosure + } + + override func performAsync(_ callback: @escaping (Result) -> Void) throws { + guard let filter = CIFilter(name: "CIQRCodeGenerator") else { + throw QRCreationOperationError.generatorUnavailable + } + + let payload = try payloadClosure() + + filter.setValue(payload, forKey: "inputMessage") + filter.setValue("M", forKey: "inputCorrectionLevel") + + guard let qrImage = filter.outputImage else { + throw QRCreationOperationError.generatedImageInvalid + } + + let transformedImage: CIImage + + if qrImage.extent.size.width * qrImage.extent.height > 0.0 { + let transform = CGAffineTransform( + scaleX: qrSize.width / qrImage.extent.width, + y: qrSize.height / qrImage.extent.height + ) + transformedImage = qrImage.transformed(by: transform) + } else { + transformedImage = qrImage + } + + let context = CIContext() + + guard let cgImage = context.createCGImage(transformedImage, from: transformedImage.extent) else { + throw QRCreationOperationError.bitmapImageCreationFailed + } + + callback(.success(UIImage(cgImage: cgImage))) + } +} diff --git a/novawallet/Common/QRCreation/NoLogo/QRCreationOperationFactory.swift b/novawallet/Common/QRCreation/NoLogo/QRCreationOperationFactory.swift new file mode 100644 index 0000000000..c94c5ff578 --- /dev/null +++ b/novawallet/Common/QRCreation/NoLogo/QRCreationOperationFactory.swift @@ -0,0 +1,11 @@ +import Operation_iOS + +protocol QRCreationOperationFactoryProtocol { + func createOperation(payload: Data, qrSize: CGSize) -> BaseOperation +} + +final class QRCreationOperationFactory: QRCreationOperationFactoryProtocol { + func createOperation(payload: Data, qrSize: CGSize) -> BaseOperation { + QRCreationOperation(payload: payload, qrSize: qrSize) + } +} diff --git a/novawallet/Common/QRCreation/QRCodeFactory.swift b/novawallet/Common/QRCreation/QRCodeFactory.swift deleted file mode 100644 index a624406dcf..0000000000 --- a/novawallet/Common/QRCreation/QRCodeFactory.swift +++ /dev/null @@ -1,203 +0,0 @@ -import Foundation -import Kingfisher -import Operation_iOS - -protocol QRCodeFactoryProtocol { - func createQRCode( - with payload: Data, - logoInfo: QRLogoInfo?, - qrSize: CGSize, - completion: @escaping (QRCodeFactory.Result) -> Void - ) -} - -final class QRCodeFactory { - enum Result: Equatable { - case full(UIImage) - case noLogo(UIImage) - - var image: UIImage { - switch self { - case let .full(image), let .noLogo(image): - return image - } - } - } - - let operationFactory: QRCreationOperationFactoryProtocol - let operationQueue: OperationQueue - - init( - operationFactory: QRCreationOperationFactoryProtocol, - operationQueue: OperationQueue - ) { - self.operationFactory = operationFactory - self.operationQueue = operationQueue - } -} - -// MARK: QRCodeFactoryProtocol - -extension QRCodeFactory: QRCodeFactoryProtocol { - func createQRCode( - with payload: Data, - logoInfo: QRLogoInfo?, - qrSize: CGSize, - completion: @escaping (Result) -> Void - ) { - let cache = KingfisherManager.shared.cache - - var cached: Bool = { - guard let logoInfo, let cacheKey = logoInfo.type?.cacheKey else { return false } - - let cachedType = cache.imageCachedType(forKey: cacheKey) - - return cachedType.cached - }() - - var usesCache: Bool { logoInfo?.type?.cacheKey != nil } - - guard let logoInfo else { - return - } - - if !cached, usesCache { - createQRCodes( - using: payload, - logoInfo: logoInfo, - qrSize: qrSize, - completion: completion - ) - } else if let cacheKey = logoInfo.type?.cacheKey { - createQRCode( - using: cache, - cacheKey: cacheKey, - payload: payload, - logoInfo: logoInfo, - qrSize: qrSize, - completion: completion - ) - } else { - createQRCode( - using: payload, - logoInfo: logoInfo, - qrSize: qrSize, - completion: completion - ) - } - } -} - -// MARK: Private - -private extension QRCodeFactory { - func createQRCode( - using payload: Data, - logoInfo: QRLogoInfo, - qrSize: CGSize, - completion: @escaping (Result) -> Void - ) { - let operation = operationFactory.createOperation( - payload: payload, - logoInfo: logoInfo, - qrSize: qrSize - ) - - execute( - [(operation: operation, withLogo: true)], - completion: completion - ) - } - - func createQRCode( - using cache: ImageCache, - cacheKey: String, - payload: Data, - logoInfo: QRLogoInfo, - qrSize: CGSize, - completion: @escaping (Result) -> Void - ) { - cache.retrieveImage(forKey: cacheKey) { [weak self] result in - guard let self else { return } - - var updatedLogoInfo: QRLogoInfo? = logoInfo - - if - case let .success(cacheResult) = result, - let image = cacheResult.image, - let type = logoInfo.type { - let localType: QRLogoType? = switch type { - case let .remoteColored(url): .localColored(image) - case let .remoteTransparent(url): .localTransparent(image) - default: nil - } - - updatedLogoInfo = QRLogoInfo( - size: logoInfo.size, - type: localType - ) - } - - let operation = operationFactory.createOperation( - payload: payload, - logoInfo: updatedLogoInfo, - qrSize: qrSize - ) - - execute( - [(operation: operation, withLogo: true)], - completion: completion - ) - } - } - - func createQRCodes( - using payload: Data, - logoInfo: QRLogoInfo, - qrSize: CGSize, - completion: @escaping (Result) -> Void - ) { - let noLogoQROperation = operationFactory.createOperation( - payload: payload, - logoInfo: QRLogoInfo(size: logoInfo.size, type: nil), - qrSize: qrSize - ) - - let embeddedLogoQROperation = operationFactory.createOperation( - payload: payload, - logoInfo: logoInfo, - qrSize: qrSize - ) - - let operations = [ - (operation: noLogoQROperation, withLogo: false), - (operation: embeddedLogoQROperation, withLogo: true) - ] - - execute(operations, completion: completion) - } - - func execute( - _ operations: [(operation: QRCreationOperation, withLogo: Bool)], - completion: @escaping (Result) -> Void - ) { - operations.forEach { operation in - novawallet.execute( - operation: operation.operation, - inOperationQueue: operationQueue, - runningCallbackIn: .main - ) { result in - switch result { - case let .success(qrCodeImage): - let qrCreationResult: Result = operation.withLogo - ? .full(qrCodeImage) - : .noLogo(qrCodeImage) - - completion(qrCreationResult) - case let .failure(error): - print(error) - } - } - } - } -} diff --git a/novawallet/Common/QRCreation/QRCreationOperation.swift b/novawallet/Common/QRCreation/QRCreationOperation.swift deleted file mode 100644 index 28c7167ab7..0000000000 --- a/novawallet/Common/QRCreation/QRCreationOperation.swift +++ /dev/null @@ -1,195 +0,0 @@ -import Foundation -import CoreImage -import Operation_iOS -import Kingfisher -import QRCode - -enum QRCreationOperationError: Error { - case generatorUnavailable - case generatedImageInvalid - case bitmapImageCreationFailed -} - -enum QRLogoType { - case remoteColored(URL?) - case remoteTransparent(URL?) - case localColored(UIImage) - case localTransparent(UIImage) - - var url: URL? { - switch self { - case let .remoteColored(url), let .remoteTransparent(url): - return url - default: - return nil - } - } - - var cacheKey: String? { - guard let url else { return nil } - - return url.absoluteString + String(describing: self) - } -} - -struct QRLogoInfo { - let size: CGSize - let type: QRLogoType? - - var url: URL? { - type?.url - } -} - -final class QRCreationOperation: BaseOperation { - let payloadClosure: () throws -> Data - let qrSize: CGSize - let logoInfo: QRLogoInfo? - - init( - payload: Data, - qrSize: CGSize, - logoInfo: QRLogoInfo? = nil - ) { - payloadClosure = { payload } - self.qrSize = qrSize - self.logoInfo = logoInfo - - super.init() - } - - init( - qrSize: CGSize, - logoInfo: QRLogoInfo? = nil, - payloadClosure: @escaping () throws -> Data - ) { - self.qrSize = qrSize - self.logoInfo = logoInfo - self.payloadClosure = payloadClosure - } - - override func performAsync(_ callback: @escaping (Result) -> Void) throws { - let data = try payloadClosure() - let qrDoc = QRCode.Document(data: data) - qrDoc.design.backgroundColor(UIColor.white.cgColor) - qrDoc.design.shape.eye = QRCode.EyeShape.Squircle() - qrDoc.design.shape.onPixels = QRCode.PixelShape.Circle(insetFraction: 0.2) - qrDoc.design.style.onPixels = QRCode.FillStyle.Solid(UIColor.black.cgColor) - qrDoc.design.shape.offPixels = nil - qrDoc.design.style.offPixels = nil - - let scale = UIScreen.main.scale - - let scaledSize = CGSize( - width: qrSize.width * scale, - height: qrSize.height * scale - ) - - let qrCreateImageClosure: () throws -> Void = { [weak self] in - guard let self else { return } - - guard let cgImage = qrDoc.cgImage(scaledSize) else { - throw BarcodeCreationError.bitmapImageCreationFailed - } - - callback(.success(UIImage(cgImage: cgImage))) - } - - guard let logoInfo else { - try qrCreateImageClosure() - return - } - - switch logoInfo.type { - case let .localColored(image): - qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter( - image: image.cgImage!, - inset: 0 - ) - - try qrCreateImageClosure() - case let .localTransparent(image): - qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter( - image: image.cgImage!, - inset: 20 - ) - - try qrCreateImageClosure() - default: - try downloadImage( - using: logoInfo, - scale: scale - ) { logoImage in - let inset: CGFloat = switch logoInfo.type { - case .remoteTransparent: 20 - default: 0 - } - - qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter( - image: logoImage, - inset: inset - ) - - try qrCreateImageClosure() - } - } - } - - private func downloadImage( - using logoInfo: QRLogoInfo, - scale: CGFloat, - completion: @escaping (CGImage) throws -> Void - ) throws { - let scaledSize = CGSize( - width: logoInfo.size.width * scale, - height: logoInfo.size.height * scale - ) - - let defaultImage = UIImage.background(from: .white, size: scaledSize)! - - guard let url = logoInfo.url else { - try completion(defaultImage.cgImage!) - - return - } - - let options: KingfisherOptionsInfo = [ - .processor(ResizingImageProcessor(referenceSize: scaledSize)), - .processor(SVGImageProcessor()) - ] - - KingfisherManager.shared.downloader.downloadImage(with: url, options: options) { result in - var resultImage: UIImage - - switch result { - case let .success(imageResult) where imageResult.image.cgImage != nil: - resultImage = imageResult.image - default: - resultImage = defaultImage - } - - let sizeBeforeProcessing = resultImage.size - - if case .remoteTransparent = logoInfo.type { - resultImage = resultImage.redrawWithBackground( - color: R.color.colorTextPrimaryOnWhite()!, - shape: .circle - ) - } - - if let cacheKey = logoInfo.type?.cacheKey { - KingfisherManager.shared.cache.store( - resultImage, - forKey: cacheKey, - options: KingfisherParsedOptionsInfo(nil) - ) - } - - try? completion(resultImage.cgImage!) - } - } -} - -extension CGSize { - static let qrLogoSize: CGSize = .init(width: 80, height: 80) -} diff --git a/novawallet/Common/QRCreation/QRDisplayView.swift b/novawallet/Common/QRCreation/QRDisplayView.swift index 388ea256ea..64230065b9 100644 --- a/novawallet/Common/QRCreation/QRDisplayView.swift +++ b/novawallet/Common/QRCreation/QRDisplayView.swift @@ -14,7 +14,7 @@ final class QRDisplayView: UIView { view.alpha = 0 } - var viewModel: QRCodeFactory.Result? + var viewModel: QRCodeWithLogoFactory.QRCreationResult? var contentInsets: CGFloat = 8.0 { didSet { @@ -49,7 +49,7 @@ final class QRDisplayView: UIView { fatalError("init(coder:) has not been implemented") } - func bind(viewModel: QRCodeFactory.Result) { + func bind(viewModel: QRCodeWithLogoFactory.QRCreationResult) { switch viewModel { case let .noLogo(image): noLogoQRImageView.image = image diff --git a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift index 41ee9cdd04..76c894022b 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift @@ -5,7 +5,7 @@ final class AssetReceiveInteractor: AnyCancellableCleaning { weak var presenter: AssetReceiveInteractorOutputProtocol! let chainAsset: ChainAsset - let qrCodeFactory: QRCodeFactoryProtocol + let qrCodeFactory: QRCodeWithLogoFactoryProtocol let qrCoderFactory: NovaWalletQRCoderFactoryProtocol let metaChainAccountResponse: MetaChainAccountResponse let appearanceFacade: AppearanceFacadeProtocol @@ -17,7 +17,7 @@ final class AssetReceiveInteractor: AnyCancellableCleaning { metaChainAccountResponse: MetaChainAccountResponse, chainAsset: ChainAsset, qrCoderFactory: NovaWalletQRCoderFactoryProtocol, - qrCodeFactory: QRCodeFactoryProtocol, + qrCodeFactory: QRCodeWithLogoFactoryProtocol, appearanceFacade: AppearanceFacadeProtocol, operationQueue: OperationQueue ) { @@ -59,10 +59,15 @@ final class AssetReceiveInteractor: AnyCancellableCleaning { logoInfo: logoInfo, qrSize: size ) { [weak self] result in - self?.presenter.didReceive(qrCodeInfo: .init( - result: result, - encodingData: receiverInfo - )) + switch result { + case let .success(qrCode): + self?.presenter.didReceive(qrCodeInfo: .init( + result: qrCode, + encodingData: receiverInfo + )) + case let .failure(error): + self?.presenter.didReceive(error: .generatingQRCode) + } } } } diff --git a/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift b/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift index 9cb8eb8085..153d7712a4 100644 --- a/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift +++ b/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift @@ -18,6 +18,7 @@ final class AssetReceivePresenter { private var account: MetaChainAccountResponse? private var chain: ChainModel? private var qrCodeSize: CGSize? + private var token: String? init( interactor: AssetReceiveInteractorInputProtocol, @@ -59,6 +60,26 @@ final class AssetReceivePresenter { view?.didReceive(networkViewModel: networkViewModel) } + + private func provideAddress() { + guard + let account, + let chain, + let token + else { + return + } + let addressViewModel = AccountAddressViewModel( + walletName: account.chainAccount.name, + address: account.chainAccount.toAddress() + ) + + view?.didReceive( + addressViewModel: addressViewModel, + networkName: chain.name, + token: token + ) + } } extension AssetReceivePresenter: AssetReceivePresenterProtocol { @@ -109,19 +130,10 @@ extension AssetReceivePresenter: AssetReceiveInteractorOutputProtocol { ) { self.account = account self.chain = chain + self.token = token provideNetwork() - - let addressViewModel = AccountAddressViewModel( - walletName: account.chainAccount.name, - address: account.chainAccount.toAddress() - ) - - view?.didReceive( - addressViewModel: addressViewModel, - networkName: chain.name, - token: token - ) + provideAddress() } func didReceive(qrCodeInfo: QRCodeInfo) { diff --git a/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift b/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift index 20ee3557a1..15eae79369 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift @@ -7,7 +7,7 @@ protocol AssetReceiveViewProtocol: ControllerBackedProtocol { networkName: String, token: String ) - func didReceive(qrResult: QRCodeFactory.Result) + func didReceive(qrResult: QRCodeWithLogoFactory.QRCreationResult) } protocol AssetReceivePresenterProtocol: AnyObject { diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift index 75814a188d..e72a426eea 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewController.swift @@ -119,7 +119,7 @@ extension AssetReceiveViewController: AssetReceiveViewProtocol { rootView.accountAddressView.addressLabel.text = addressViewModel.address } - func didReceive(qrResult: QRCodeFactory.Result) { + func didReceive(qrResult: QRCodeWithLogoFactory.QRCreationResult) { rootView.qrView.bind(viewModel: qrResult) } diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift index 5157c27a56..f8a5037b9d 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift @@ -15,9 +15,9 @@ struct AssetReceiveViewFactory { let operationQueue = OperationManagerFacade.sharedDefaultQueue - let qrCodeFactory = QRCodeFactory( - operationFactory: QRCreationOperationFactory(), - operationQueue: operationQueue + let qrCodeFactory = QRCodeWithLogoFactory( + operationQueue: operationQueue, + logger: Logger.shared ) let interactor = AssetReceiveInteractor( diff --git a/novawallet/Modules/AssetReceive/Model/QRCodeInfo.swift b/novawallet/Modules/AssetReceive/Model/QRCodeInfo.swift index 84893a322e..bac7173c5d 100644 --- a/novawallet/Modules/AssetReceive/Model/QRCodeInfo.swift +++ b/novawallet/Modules/AssetReceive/Model/QRCodeInfo.swift @@ -1,6 +1,6 @@ import UIKit struct QRCodeInfo { - let result: QRCodeFactory.Result + let result: QRCodeWithLogoFactory.QRCreationResult let encodingData: AssetReceiveInfo } diff --git a/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift b/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift deleted file mode 100644 index 5993e58a6f..0000000000 --- a/novawallet/Modules/AssetReceive/QRCreationOperationFactory.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Operation_iOS - -protocol QRCreationOperationFactoryProtocol { - func createOperation( - payload: Data, - logoInfo: QRLogoInfo?, - qrSize: CGSize - ) -> QRCreationOperation -} - -final class QRCreationOperationFactory: QRCreationOperationFactoryProtocol { - func createOperation( - payload: Data, - logoInfo: QRLogoInfo?, - qrSize: CGSize - ) -> QRCreationOperation { - QRCreationOperation( - payload: payload, - qrSize: qrSize, - logoInfo: logoInfo - ) - } -} diff --git a/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift b/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift index 54e1b818dd..d54ff3bc76 100644 --- a/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift +++ b/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrInteractor.swift @@ -59,7 +59,7 @@ final class ParitySignerTxQrInteractor { let paylods = try qrPayloadWrapper.targetOperation.extractNoCancellableResultData() return paylods.map { payload in - let operation = QRCreationOperation(payload: payload, qrSize: size, logoInfo: nil) + let operation = QRCreationOperation(payload: payload, qrSize: size) return CompoundOperationWrapper(targetOperation: operation) } }.longrunOperation() From 0ddee7c2ae26286f0ec6a8c26405b4ad1c3ee50f Mon Sep 17 00:00:00 2001 From: svojsu Date: Thu, 31 Oct 2024 23:56:23 +0200 Subject: [PATCH 08/12] add CopyAddressPresentable protocol --- novawallet.xcodeproj/project.pbxproj | 4 +++ .../Protocols/CopyAddressPresentable.swift | 26 +++++++++++++++++++ .../AssetReceive/AssetReceivePresenter.swift | 13 +++++----- .../AssetReceive/AssetReceiveProtocols.swift | 2 +- 4 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 novawallet/Common/Protocols/CopyAddressPresentable.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index bcc5d0b0b0..59501d4468 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -1076,6 +1076,7 @@ 2DBB288D2CD42C5200DFA0AD /* QRCreationOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB288C2CD42C5200DFA0AD /* QRCreationOperationFactory.swift */; }; 2DBB288F2CD42E8D00DFA0AD /* QRLogoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB288E2CD42E8D00DFA0AD /* QRLogoType.swift */; }; 2DBB28912CD42ED800DFA0AD /* QRLogoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28902CD42ED800DFA0AD /* QRLogoInfo.swift */; }; + 2DBB28932CD432FB00DFA0AD /* CopyAddressPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28922CD432FB00DFA0AD /* CopyAddressPresentable.swift */; }; 2DC075E32BF24AB400868563 /* CheckBoxIconDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC075E22BF24AB400868563 /* CheckBoxIconDetailsView.swift */; }; 2DC075E92BF4B97900868563 /* BackupAttentionTableTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC075E82BF4B97900868563 /* BackupAttentionTableTitleView.swift */; }; 2DCC875B2C5820BA0028C3CA /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849013D424A927E2008F705E /* Logger.swift */; }; @@ -6258,6 +6259,7 @@ 2DBB288C2CD42C5200DFA0AD /* QRCreationOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCreationOperationFactory.swift; sourceTree = ""; }; 2DBB288E2CD42E8D00DFA0AD /* QRLogoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRLogoType.swift; sourceTree = ""; }; 2DBB28902CD42ED800DFA0AD /* QRLogoInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRLogoInfo.swift; sourceTree = ""; }; + 2DBB28922CD432FB00DFA0AD /* CopyAddressPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyAddressPresentable.swift; sourceTree = ""; }; 2DC075E22BF24AB400868563 /* CheckBoxIconDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBoxIconDetailsView.swift; sourceTree = ""; }; 2DC075E82BF4B97900868563 /* BackupAttentionTableTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAttentionTableTitleView.swift; sourceTree = ""; }; 2DC70DB97D2E9350022A899B /* TokensManagePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TokensManagePresenter.swift; sourceTree = ""; }; @@ -17876,6 +17878,7 @@ 842D1E8324D197C900C30A7A /* KeyboardAdoptable.swift */, 84754C842510A1A400854599 /* ModalAlertPresenting.swift */, 849DEC6025EE13CE00C64C19 /* AddressOptionsPresentable.swift */, + 2DBB28922CD432FB00DFA0AD /* CopyAddressPresentable.swift */, 847DD8DB26034B99003DE053 /* LocalizableViewProtocol.swift */, 842348E82614F6EA002127AF /* SkeletonLoadable.swift */, 2D6AED5A2C05C3BB001A0A15 /* MnemonicFetching.swift */, @@ -26779,6 +26782,7 @@ 848B3000286EDE3800465BA2 /* ParaIdOperationFactory.swift in Sources */, 84F47D4B2666EF1C00F7647A /* KaruraStatementData.swift in Sources */, 0CEB6B4D2CA6676900609DC2 /* VoteCardPanState.swift in Sources */, + 2DBB28932CD432FB00DFA0AD /* CopyAddressPresentable.swift in Sources */, 84D8753A28EB0A93004065BD /* GovernanceChainSettings.swift in Sources */, 8443FE24255586230092893D /* ExportMnemonicConfirmProtocols.swift in Sources */, 77DF404B2B7FFBA400ABDB53 /* ChainNotificationsSettingsProtocols.swift in Sources */, diff --git a/novawallet/Common/Protocols/CopyAddressPresentable.swift b/novawallet/Common/Protocols/CopyAddressPresentable.swift new file mode 100644 index 0000000000..443beb4dfe --- /dev/null +++ b/novawallet/Common/Protocols/CopyAddressPresentable.swift @@ -0,0 +1,26 @@ +import UIKit + +protocol CopyAddressPresentable { + func copyAddress( + from view: ControllerBackedProtocol?, + address: String, + locale: Locale + ) +} + +extension CopyAddressPresentable where Self: ModalAlertPresenting { + func copyAddress( + from view: ControllerBackedProtocol?, + address: String, + locale: Locale + ) { + UIPasteboard.general.string = address + + let title = R.string.localizable.commonAddressCoppied(preferredLanguages: locale.rLanguages) + + presentSuccessNotification( + title, + from: view + ) + } +} diff --git a/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift b/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift index 153d7712a4..0ba326dbf2 100644 --- a/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift +++ b/novawallet/Modules/AssetReceive/AssetReceivePresenter.swift @@ -60,9 +60,9 @@ final class AssetReceivePresenter { view?.didReceive(networkViewModel: networkViewModel) } - + private func provideAddress() { - guard + guard let account, let chain, let token @@ -113,11 +113,10 @@ extension AssetReceivePresenter: AssetReceivePresenterProtocol { return } - UIPasteboard.general.string = address - - wireframe.presentSuccessNotification( - R.string.localizable.commonAddressCoppied(), - from: view + wireframe.copyAddress( + from: view, + address: address, + locale: localizationManager.selectedLocale ) } } diff --git a/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift b/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift index 15eae79369..6aebe14252 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveProtocols.swift @@ -29,4 +29,4 @@ protocol AssetReceiveInteractorOutputProtocol: AnyObject { } protocol AssetReceiveWireframeProtocol: AnyObject, SharingPresentable, - ErrorPresentable, AlertPresentable, CommonRetryable, ModalAlertPresenting {} + ErrorPresentable, AlertPresentable, CommonRetryable, ModalAlertPresenting, CopyAddressPresentable {} From f74ceca0a10fbaa3d863c2658436d7d37d98ae7c Mon Sep 17 00:00:00 2001 From: svojsu Date: Sun, 3 Nov 2024 15:18:25 +0200 Subject: [PATCH 09/12] add partialResultClosure logic for QR code factory --- .../Logo/QRCodeWithLogoFactory.swift | 49 ++++++++++++++++--- .../AssetReceive/AssetReceiveInteractor.swift | 16 +++--- .../AssetReceiveViewFactory.swift | 1 + 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift b/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift index 3f3d0376c5..d91c82be0f 100644 --- a/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift +++ b/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift @@ -12,6 +12,7 @@ protocol QRCodeWithLogoFactoryProtocol { with payload: Data, logoInfo: QRLogoInfo?, qrSize: CGSize, + partialResultClosure: @escaping (Result) -> Void, completion: @escaping (Result) -> Void ) } @@ -31,15 +32,18 @@ final class QRCodeWithLogoFactory { let imageManager: KingfisherManager let operationQueue: OperationQueue + let callbackQueue: DispatchQueue let logger: LoggerProtocol init( imageManager: KingfisherManager = KingfisherManager.shared, operationQueue: OperationQueue, + callbackQueue: DispatchQueue, logger: LoggerProtocol ) { self.imageManager = imageManager self.operationQueue = operationQueue + self.callbackQueue = callbackQueue self.logger = logger } } @@ -51,12 +55,14 @@ extension QRCodeWithLogoFactory: QRCodeWithLogoFactoryProtocol { with payload: Data, logoInfo: QRLogoInfo?, qrSize: CGSize, + partialResultClosure: @escaping (Result) -> Void, completion: @escaping (Result) -> Void ) { let wrapper = createQRWrapper( with: payload, logoInfo: logoInfo, - qrSize: qrSize + qrSize: qrSize, + partialResultClosure: partialResultClosure ) novawallet.execute( @@ -97,14 +103,13 @@ private extension QRCodeWithLogoFactory { func createQRWrapper( with payload: Data, logoInfo: QRLogoInfo?, - qrSize: CGSize + qrSize: CGSize, + partialResultClosure: @escaping (Result) -> Void ) -> CompoundOperationWrapper { let cache = imageManager.cache let checkCacheOperation = checkCacheOperation(in: cache, using: logoInfo) - var remoteLogo = false - let wrapper: CompoundOperationWrapper = OperationCombiningService.compoundNonOptionalWrapper( operationManager: OperationManager(operationQueue: operationQueue) ) { [weak self] in @@ -120,7 +125,8 @@ private extension QRCodeWithLogoFactory { payload: payload, downloader: imageManager.downloader, logoInfo: logoInfo, - qrSize: qrSize + qrSize: qrSize, + partialResultClosure: partialResultClosure ) } else if let cacheKey = logoInfo.type?.cacheKey { createCachedLogoQRWrapper( @@ -156,19 +162,44 @@ private extension QRCodeWithLogoFactory { payload: Data, downloader: ImageDownloader, logoInfo: QRLogoInfo, - qrSize: CGSize + qrSize: CGSize, + partialResultClosure: @escaping (Result) -> Void ) -> CompoundOperationWrapper { let logoOperation = downloadImageOperation( using: logoInfo, downloader: downloader ) - return createQRResultWrapper( + let partialQRWrapper = createQRResultWrapper( + using: nil, + payload: payload, + logoInfo: logoInfo, + qrSize: qrSize + ) + + partialQRWrapper.targetOperation.completionBlock = { [weak self] in + guard let self else { return } + + dispatchInQueueWhenPossible(callbackQueue) { + do { + let value = try partialQRWrapper.targetOperation.extractNoCancellableResultData() + partialResultClosure(.success(value)) + } catch { + partialResultClosure(.failure(error)) + } + } + } + + let completeQRWrapper = createQRResultWrapper( using: logoOperation, payload: payload, logoInfo: logoInfo, qrSize: qrSize ) + + completeQRWrapper.targetOperation.addDependency(partialQRWrapper.targetOperation) + + return completeQRWrapper.insertingHead(operations: partialQRWrapper.allOperations) } func createCachedLogoQRWrapper( @@ -214,6 +245,8 @@ private extension QRCodeWithLogoFactory { } catch { logger.error(error.localizedDescription) } + } else { + updatedLogoInfo = updatedLogoInfo?.withNoLogo() } let qrCodeOperation = QRWithLogoCreationOperation( @@ -236,7 +269,7 @@ private extension QRCodeWithLogoFactory { ) { let qrImage = try qrImageWrapper.targetOperation.extractNoCancellableResultData() - let result: QRCreationResult = if logoInfo.type != nil { + let result: QRCreationResult = if logoOperation != nil { .full(qrImage) } else { .noLogo(qrImage) diff --git a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift index 76c894022b..3557c4632d 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift @@ -54,21 +54,25 @@ final class AssetReceiveInteractor: AnyCancellableCleaning { type: qrLogoType ) - qrCodeFactory.createQRCode( - with: payload, - logoInfo: logoInfo, - qrSize: size - ) { [weak self] result in + let resultClosure: (Result) -> Void = { [weak self] result in switch result { case let .success(qrCode): self?.presenter.didReceive(qrCodeInfo: .init( result: qrCode, encodingData: receiverInfo )) - case let .failure(error): + case .failure: self?.presenter.didReceive(error: .generatingQRCode) } } + + qrCodeFactory.createQRCode( + with: payload, + logoInfo: logoInfo, + qrSize: size, + partialResultClosure: resultClosure, + completion: resultClosure + ) } } diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift index f8a5037b9d..efddcd52ea 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift @@ -17,6 +17,7 @@ struct AssetReceiveViewFactory { let qrCodeFactory = QRCodeWithLogoFactory( operationQueue: operationQueue, + callbackQueue: .main, logger: Logger.shared ) From dfde9834ac94691593ab059c3b7f5556f99dee5f Mon Sep 17 00:00:00 2001 From: svojsu Date: Sun, 3 Nov 2024 23:22:59 +0200 Subject: [PATCH 10/12] add IconRetrieveOperationFactoryProtocol and Kingfisher implementation --- novawallet.xcodeproj/project.pbxproj | 28 +++- .../IconRetrieveOperationFactory.swift | 112 ++++++++++++++ .../Logo/{QRLogoInfo.swift => IconInfo.swift} | 16 +- .../Logo/{QRLogoType.swift => IconType.swift} | 2 +- .../Logo/QRCodeWithLogoFactory.swift | 140 +++--------------- .../Logo/QRWithLogoCreationOperation.swift | 6 +- .../AssetReceive/AssetReceiveInteractor.swift | 2 +- .../AssetReceiveViewFactory.swift | 3 + .../Model/AssetIconURLFactory.swift | 2 +- 9 files changed, 171 insertions(+), 140 deletions(-) create mode 100644 novawallet/Common/IconRetrieve/IconRetrieveOperationFactory.swift rename novawallet/Common/QRCreation/Logo/{QRLogoInfo.swift => IconInfo.swift} (68%) rename novawallet/Common/QRCreation/Logo/{QRLogoType.swift => IconType.swift} (97%) diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 59501d4468..09b797ae0a 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -932,6 +932,7 @@ 2D1C5D262C25ACE900E2DBDD /* CustomNetworkEditPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1C5D252C25ACE900E2DBDD /* CustomNetworkEditPresenter.swift */; }; 2D1C5D282C25ACF200E2DBDD /* CustomNetworkEditInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1C5D272C25ACF200E2DBDD /* CustomNetworkEditInteractor.swift */; }; 2D1C5D2A2C25AD0200E2DBDD /* CustomNetworkPresenterProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1C5D292C25AD0200E2DBDD /* CustomNetworkPresenterProtocols.swift */; }; + 2D1D66002CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1D65FF2CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift */; }; 2D2F6F522C50E52D005020EF /* VotingCurveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D2F6F512C50E52D005020EF /* VotingCurveTests.swift */; }; 2D32BE122C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */; }; 2D32BE132C6A49900047F520 /* ExtrinsicAssetConversionFeeInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32BE062C6A49900047F520 /* ExtrinsicAssetConversionFeeInstaller.swift */; }; @@ -1074,8 +1075,8 @@ 2DBB28872CD2934500DFA0AD /* AssetIconURLFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28862CD2934500DFA0AD /* AssetIconURLFactory.swift */; }; 2DBB28892CD42ACB00DFA0AD /* QRCreationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28882CD42ACB00DFA0AD /* QRCreationOperation.swift */; }; 2DBB288D2CD42C5200DFA0AD /* QRCreationOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB288C2CD42C5200DFA0AD /* QRCreationOperationFactory.swift */; }; - 2DBB288F2CD42E8D00DFA0AD /* QRLogoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB288E2CD42E8D00DFA0AD /* QRLogoType.swift */; }; - 2DBB28912CD42ED800DFA0AD /* QRLogoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28902CD42ED800DFA0AD /* QRLogoInfo.swift */; }; + 2DBB288F2CD42E8D00DFA0AD /* IconType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB288E2CD42E8D00DFA0AD /* IconType.swift */; }; + 2DBB28912CD42ED800DFA0AD /* IconInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28902CD42ED800DFA0AD /* IconInfo.swift */; }; 2DBB28932CD432FB00DFA0AD /* CopyAddressPresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBB28922CD432FB00DFA0AD /* CopyAddressPresentable.swift */; }; 2DC075E32BF24AB400868563 /* CheckBoxIconDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC075E22BF24AB400868563 /* CheckBoxIconDetailsView.swift */; }; 2DC075E92BF4B97900868563 /* BackupAttentionTableTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DC075E82BF4B97900868563 /* BackupAttentionTableTitleView.swift */; }; @@ -6116,6 +6117,7 @@ 2D1C5D252C25ACE900E2DBDD /* CustomNetworkEditPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNetworkEditPresenter.swift; sourceTree = ""; }; 2D1C5D272C25ACF200E2DBDD /* CustomNetworkEditInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNetworkEditInteractor.swift; sourceTree = ""; }; 2D1C5D292C25AD0200E2DBDD /* CustomNetworkPresenterProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNetworkPresenterProtocols.swift; sourceTree = ""; }; + 2D1D65FF2CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconRetrieveOperationFactory.swift; sourceTree = ""; }; 2D2F6F512C50E52D005020EF /* VotingCurveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingCurveTests.swift; sourceTree = ""; }; 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtrinsicAssetConversionFeeEstimator.swift; sourceTree = ""; }; 2D32BE062C6A49900047F520 /* ExtrinsicAssetConversionFeeInstaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtrinsicAssetConversionFeeInstaller.swift; sourceTree = ""; }; @@ -6257,8 +6259,8 @@ 2DBB28862CD2934500DFA0AD /* AssetIconURLFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetIconURLFactory.swift; sourceTree = ""; }; 2DBB28882CD42ACB00DFA0AD /* QRCreationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCreationOperation.swift; sourceTree = ""; }; 2DBB288C2CD42C5200DFA0AD /* QRCreationOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCreationOperationFactory.swift; sourceTree = ""; }; - 2DBB288E2CD42E8D00DFA0AD /* QRLogoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRLogoType.swift; sourceTree = ""; }; - 2DBB28902CD42ED800DFA0AD /* QRLogoInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRLogoInfo.swift; sourceTree = ""; }; + 2DBB288E2CD42E8D00DFA0AD /* IconType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconType.swift; sourceTree = ""; }; + 2DBB28902CD42ED800DFA0AD /* IconInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconInfo.swift; sourceTree = ""; }; 2DBB28922CD432FB00DFA0AD /* CopyAddressPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyAddressPresentable.swift; sourceTree = ""; }; 2DC075E22BF24AB400868563 /* CheckBoxIconDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckBoxIconDetailsView.swift; sourceTree = ""; }; 2DC075E82BF4B97900868563 /* BackupAttentionTableTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAttentionTableTitleView.swift; sourceTree = ""; }; @@ -12496,6 +12498,14 @@ path = Interactor; sourceTree = ""; }; + 2D1D65FE2CD80A33009C6C2F /* IconRetrieve */ = { + isa = PBXGroup; + children = ( + 2D1D65FF2CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift */, + ); + path = IconRetrieve; + sourceTree = ""; + }; 2D32BE112C6A49900047F520 /* FeeManaging */ = { isa = PBXGroup; children = ( @@ -12921,8 +12931,8 @@ children = ( 84BC703E289DBE6C008A9758 /* QRWithLogoCreationOperation.swift */, 2DBB28842CD2811200DFA0AD /* QRCodeWithLogoFactory.swift */, - 2DBB288E2CD42E8D00DFA0AD /* QRLogoType.swift */, - 2DBB28902CD42ED800DFA0AD /* QRLogoInfo.swift */, + 2DBB288E2CD42E8D00DFA0AD /* IconType.swift */, + 2DBB28902CD42ED800DFA0AD /* IconInfo.swift */, ); path = Logo; sourceTree = ""; @@ -17607,6 +17617,7 @@ 849013D124A92686008F705E /* Common */ = { isa = PBXGroup; children = ( + 2D1D65FE2CD80A33009C6C2F /* IconRetrieve */, 0CCDB2E92B74DA33007BC5D6 /* Graphs */, 0C9C642B2A8CE2D4004DC078 /* SystemAccounts */, 0C463FCE2A592ACD003E71C9 /* Effects */, @@ -25938,7 +25949,7 @@ 88D997B028ABC8C0006135A5 /* YourContributionsTableViewCell.swift in Sources */, 849ABE5B2627739400011A2A /* ListReducing.swift in Sources */, 0C7CF4CC2BD374930015DD45 /* CloudBackupFileManaging.swift in Sources */, - 2DBB28912CD42ED800DFA0AD /* QRLogoInfo.swift in Sources */, + 2DBB28912CD42ED800DFA0AD /* IconInfo.swift in Sources */, 88C5F07C297EE79C001CCADE /* Release.swift in Sources */, 845B89222959620000EE25B0 /* SecurityLayerPresenter.swift in Sources */, 849E07F4284A04F400DE0440 /* ParaStkAccountSubscribeHandlingFactory.swift in Sources */, @@ -27189,6 +27200,7 @@ 8463A73825E3AA47003B8160 /* AccountInfo.swift in Sources */, ABA3D873BBECB7F4BD670872 /* ExportSeedPresenter.swift in Sources */, 8476D39B27F43E30004D9A7A /* PhishingSiteVerifier.swift in Sources */, + 2D1D66002CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift in Sources */, 0C7CF4D62BD4E5350015DD45 /* KeychainProxy.swift in Sources */, 9A6A55297F41DAE45071BF57 /* ExportSeedInteractor.swift in Sources */, 0CE933C02C068BF2000F3EFE /* AddAccount+ImportOptionsPresenter.swift in Sources */, @@ -27474,7 +27486,7 @@ 7725062C2A1C99DB00E653DB /* ReferendumSearchViewLayout.swift in Sources */, 84466B3528B6731B00FA1E0D /* LedgerSigningWrapper.swift in Sources */, 0C38B4FB2B7A5DB500882A8B /* ExtrinsicProcessor+HydraSwapMatching.swift in Sources */, - 2DBB288F2CD42E8D00DFA0AD /* QRLogoType.swift in Sources */, + 2DBB288F2CD42E8D00DFA0AD /* IconType.swift in Sources */, F4223ED127329767003D8E4E /* AcalaTransferRequest.swift in Sources */, 8473F4B8282BFFF8007CC55A /* StakingRelaychainInteractor+Subscription.swift in Sources */, 841E2E5027381B2A00F250C1 /* AccountInfoSubscriptionHandlingFactory.swift in Sources */, diff --git a/novawallet/Common/IconRetrieve/IconRetrieveOperationFactory.swift b/novawallet/Common/IconRetrieve/IconRetrieveOperationFactory.swift new file mode 100644 index 0000000000..5463fbb1c5 --- /dev/null +++ b/novawallet/Common/IconRetrieve/IconRetrieveOperationFactory.swift @@ -0,0 +1,112 @@ +import Foundation +import Kingfisher +import Operation_iOS + +protocol IconRetrieveOperationFactoryProtocol { + func checkCacheOperation(using cacheKey: String) -> ClosureOperation + + func downloadImageOperation( + using logoInfo: IconInfo + ) -> AsyncClosureOperation + + func retrieveImageOperation(using cacheKey: String) -> AsyncClosureOperation +} + +struct KingfisherIconRetrieveOperationFactory { + let imageManager: KingfisherManager + let operationQueue: OperationQueue + + init( + imageManager: KingfisherManager = KingfisherManager.shared, + operationQueue: OperationQueue + ) { + self.imageManager = imageManager + self.operationQueue = operationQueue + } +} + +// MARK: IconRetrieveOperationFactoryProtocol + +extension KingfisherIconRetrieveOperationFactory: IconRetrieveOperationFactoryProtocol { + func checkCacheOperation(using cacheKey: String) -> Operation_iOS.ClosureOperation { + let cache = imageManager.cache + + return ClosureOperation { + let cachedType = cache.imageCachedType(forKey: cacheKey) + + return cachedType.cached + } + } + + func downloadImageOperation(using iconInfo: IconInfo) -> Operation_iOS.AsyncClosureOperation { + let downloader = imageManager.downloader + + return AsyncClosureOperation { resultClosure in + let scale = UIScreen.main.scale + + let scaledSize = CGSize( + width: iconInfo.size.width * scale, + height: iconInfo.size.height * scale + ) + + guard let url = iconInfo.url else { + resultClosure(.failure(QRCodeFactoryError.logoDownloadError)) + + return + } + + let options: KingfisherOptionsInfo = [ + .processor(ResizingImageProcessor(referenceSize: scaledSize)), + .processor(SVGImageProcessor()) + ] + + downloader.downloadImage(with: url, options: options) { result in + var resultImage: UIImage + + switch result { + case let .success(imageResult) where imageResult.image.cgImage != nil: + resultImage = imageResult.image + default: + resultClosure(.failure(QRCodeFactoryError.logoDownloadError)) + + return + } + + let sizeBeforeProcessing = resultImage.size + + if case .remoteTransparent = iconInfo.type { + resultImage = resultImage.redrawWithBackground( + color: R.color.colorTextPrimaryOnWhite()!, + shape: .circle + ) + } + + if let cacheKey = iconInfo.type?.cacheKey { + KingfisherManager.shared.cache.store( + resultImage, + forKey: cacheKey, + options: KingfisherParsedOptionsInfo(nil) + ) + } + + resultClosure(.success(resultImage)) + } + } + } + + func retrieveImageOperation(using cacheKey: String) -> Operation_iOS.AsyncClosureOperation { + let cache = imageManager.cache + + return AsyncClosureOperation { resultClosure in + cache.retrieveImage(forKey: cacheKey) { result in + if + case let .success(cacheResult) = result, + let image = cacheResult.image { + resultClosure(.success(image)) + } else { + resultClosure(.failure(QRCodeFactoryError.logoRetrievingError)) + } + } + } + } +} diff --git a/novawallet/Common/QRCreation/Logo/QRLogoInfo.swift b/novawallet/Common/QRCreation/Logo/IconInfo.swift similarity index 68% rename from novawallet/Common/QRCreation/Logo/QRLogoInfo.swift rename to novawallet/Common/QRCreation/Logo/IconInfo.swift index 72f3305a60..bf8f2332f2 100644 --- a/novawallet/Common/QRCreation/Logo/QRLogoInfo.swift +++ b/novawallet/Common/QRCreation/Logo/IconInfo.swift @@ -1,36 +1,36 @@ import Foundation import UIKit -struct QRLogoInfo { +struct IconInfo { let size: CGSize - let type: QRLogoType? + let type: IconType? var url: URL? { type?.url } - func byChangingToLocal(_ image: UIImage) -> QRLogoInfo? { + func byChangingToLocal(_ image: UIImage) -> IconInfo? { switch type { case .remoteColored: - QRLogoInfo( + IconInfo( size: size, type: .localColored(image) ) case .remoteTransparent: - QRLogoInfo( + IconInfo( size: size, type: .localTransparent(image) ) default: - QRLogoInfo( + IconInfo( size: size, type: nil ) } } - func withNoLogo() -> QRLogoInfo { - QRLogoInfo( + func withNoLogo() -> IconInfo { + IconInfo( size: size, type: nil ) diff --git a/novawallet/Common/QRCreation/Logo/QRLogoType.swift b/novawallet/Common/QRCreation/Logo/IconType.swift similarity index 97% rename from novawallet/Common/QRCreation/Logo/QRLogoType.swift rename to novawallet/Common/QRCreation/Logo/IconType.swift index 24e08b6cc5..a48db68a9b 100644 --- a/novawallet/Common/QRCreation/Logo/QRLogoType.swift +++ b/novawallet/Common/QRCreation/Logo/IconType.swift @@ -1,7 +1,7 @@ import Foundation import UIKit -enum QRLogoType { +enum IconType { case remoteColored(URL?) case remoteTransparent(URL?) case localColored(UIImage) diff --git a/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift b/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift index d91c82be0f..397bc127be 100644 --- a/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift +++ b/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift @@ -10,7 +10,7 @@ enum QRCodeFactoryError: Error { protocol QRCodeWithLogoFactoryProtocol { func createQRCode( with payload: Data, - logoInfo: QRLogoInfo?, + logoInfo: IconInfo?, qrSize: CGSize, partialResultClosure: @escaping (Result) -> Void, completion: @escaping (Result) -> Void @@ -30,18 +30,18 @@ final class QRCodeWithLogoFactory { } } - let imageManager: KingfisherManager + let iconRetrievingFactory: IconRetrieveOperationFactoryProtocol let operationQueue: OperationQueue let callbackQueue: DispatchQueue let logger: LoggerProtocol init( - imageManager: KingfisherManager = KingfisherManager.shared, + iconRetrievingFactory: IconRetrieveOperationFactoryProtocol, operationQueue: OperationQueue, callbackQueue: DispatchQueue, logger: LoggerProtocol ) { - self.imageManager = imageManager + self.iconRetrievingFactory = iconRetrievingFactory self.operationQueue = operationQueue self.callbackQueue = callbackQueue self.logger = logger @@ -53,7 +53,7 @@ final class QRCodeWithLogoFactory { extension QRCodeWithLogoFactory: QRCodeWithLogoFactoryProtocol { func createQRCode( with payload: Data, - logoInfo: QRLogoInfo?, + logoInfo: IconInfo?, qrSize: CGSize, partialResultClosure: @escaping (Result) -> Void, completion: @escaping (Result) -> Void @@ -82,33 +82,21 @@ private extension QRCodeWithLogoFactory { let remoteLogo: Bool } - func checkCacheOperation( - in cache: ImageCache, - using logoInfo: QRLogoInfo? - ) -> ClosureOperation { - ClosureOperation { - guard - let logoInfo, - let cacheKey = logoInfo.type?.cacheKey - else { - return false - } - - let cachedType = cache.imageCachedType(forKey: cacheKey) - - return cachedType.cached + func checkCacheOperation(using logoInfo: IconInfo?) -> BaseOperation { + if let cacheKey = logoInfo?.type?.cacheKey { + iconRetrievingFactory.checkCacheOperation(using: cacheKey) + } else { + .createWithResult(false) } } func createQRWrapper( with payload: Data, - logoInfo: QRLogoInfo?, + logoInfo: IconInfo?, qrSize: CGSize, partialResultClosure: @escaping (Result) -> Void ) -> CompoundOperationWrapper { - let cache = imageManager.cache - - let checkCacheOperation = checkCacheOperation(in: cache, using: logoInfo) + var checkCacheOperation = checkCacheOperation(using: logoInfo) let wrapper: CompoundOperationWrapper = OperationCombiningService.compoundNonOptionalWrapper( operationManager: OperationManager(operationQueue: operationQueue) @@ -123,14 +111,12 @@ private extension QRCodeWithLogoFactory { return if !cached, usesCache { createDownloadLogoQRWrapper( payload: payload, - downloader: imageManager.downloader, logoInfo: logoInfo, qrSize: qrSize, partialResultClosure: partialResultClosure ) } else if let cacheKey = logoInfo.type?.cacheKey { createCachedLogoQRWrapper( - using: cache, cacheKey: cacheKey, payload: payload, logoInfo: logoInfo, @@ -153,22 +139,20 @@ private extension QRCodeWithLogoFactory { } } - wrapper.addDependency(operations: [checkCacheOperation]) + let dependencies = [checkCacheOperation].compactMap { $0 } - return wrapper.insertingHead(operations: [checkCacheOperation]) + wrapper.addDependency(operations: dependencies) + + return wrapper.insertingHead(operations: dependencies) } func createDownloadLogoQRWrapper( payload: Data, - downloader: ImageDownloader, - logoInfo: QRLogoInfo, + logoInfo: IconInfo, qrSize: CGSize, partialResultClosure: @escaping (Result) -> Void ) -> CompoundOperationWrapper { - let logoOperation = downloadImageOperation( - using: logoInfo, - downloader: downloader - ) + let logoOperation = iconRetrievingFactory.downloadImageOperation(using: logoInfo) let partialQRWrapper = createQRResultWrapper( using: nil, @@ -203,16 +187,12 @@ private extension QRCodeWithLogoFactory { } func createCachedLogoQRWrapper( - using cache: ImageCache, cacheKey: String, payload: Data, - logoInfo: QRLogoInfo, + logoInfo: IconInfo, qrSize: CGSize ) -> CompoundOperationWrapper { - let logoOperation = retrieveImageOperation( - using: cache, - cacheKey: cacheKey - ) + let logoOperation = iconRetrievingFactory.retrieveImageOperation(using: cacheKey) return createQRResultWrapper( using: logoOperation, @@ -225,7 +205,7 @@ private extension QRCodeWithLogoFactory { func createQRResultWrapper( using logoOperation: BaseOperation?, payload: Data, - logoInfo: QRLogoInfo, + logoInfo: IconInfo, qrSize: CGSize ) -> CompoundOperationWrapper { var qrImageWrapper: CompoundOperationWrapper @@ -236,7 +216,7 @@ private extension QRCodeWithLogoFactory { return .createWithError(BaseOperationError.parentOperationCancelled) } - var updatedLogoInfo: QRLogoInfo? = logoInfo + var updatedLogoInfo: IconInfo? = logoInfo if let logoOperation { do { @@ -282,80 +262,4 @@ private extension QRCodeWithLogoFactory { return resultMappingWrapper.insertingHead(operations: qrImageWrapper.allOperations) } - - func downloadImageOperation( - using logoInfo: QRLogoInfo, - downloader: ImageDownloader - ) -> AsyncClosureOperation { - AsyncClosureOperation { resultClosure in - let scale = UIScreen.main.scale - - let scaledSize = CGSize( - width: logoInfo.size.width * scale, - height: logoInfo.size.height * scale - ) - - guard let url = logoInfo.url else { - resultClosure(.failure(QRCodeFactoryError.logoDownloadError)) - - return - } - - let options: KingfisherOptionsInfo = [ - .processor(ResizingImageProcessor(referenceSize: scaledSize)), - .processor(SVGImageProcessor()) - ] - - downloader.downloadImage(with: url, options: options) { result in - var resultImage: UIImage - - switch result { - case let .success(imageResult) where imageResult.image.cgImage != nil: - resultImage = imageResult.image - default: - resultClosure(.failure(QRCodeFactoryError.logoDownloadError)) - - return - } - - let sizeBeforeProcessing = resultImage.size - - if case .remoteTransparent = logoInfo.type { - resultImage = resultImage.redrawWithBackground( - color: R.color.colorTextPrimaryOnWhite()!, - shape: .circle - ) - } - - if let cacheKey = logoInfo.type?.cacheKey { - KingfisherManager.shared.cache.store( - resultImage, - forKey: cacheKey, - options: KingfisherParsedOptionsInfo(nil) - ) - } - - resultClosure(.success(resultImage)) - } - } - } - - func retrieveImageOperation( - using cache: ImageCache, - cacheKey: String - ) -> AsyncClosureOperation { - AsyncClosureOperation { resultClosure in - cache.retrieveImage(forKey: cacheKey) { [weak self] result in - guard let self else { return } - - if - case let .success(cacheResult) = result, - let image = cacheResult.image { - resultClosure(.success(image)) - } else { - resultClosure(.failure(QRCodeFactoryError.logoRetrievingError)) - } - } - } - } } diff --git a/novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift b/novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift index a71a49669f..a4038ad7cb 100644 --- a/novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift +++ b/novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift @@ -7,12 +7,12 @@ import QRCode final class QRWithLogoCreationOperation: BaseOperation { let payloadClosure: () throws -> Data let qrSize: CGSize - let logoInfo: QRLogoInfo? + let logoInfo: IconInfo? init( payload: Data, qrSize: CGSize, - logoInfo: QRLogoInfo? = nil + logoInfo: IconInfo? = nil ) { payloadClosure = { payload } self.qrSize = qrSize @@ -23,7 +23,7 @@ final class QRWithLogoCreationOperation: BaseOperation { init( qrSize: CGSize, - logoInfo: QRLogoInfo? = nil, + logoInfo: IconInfo? = nil, payloadClosure: @escaping () throws -> Data ) { self.qrSize = qrSize diff --git a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift index 3557c4632d..ebece54f2f 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveInteractor.swift @@ -49,7 +49,7 @@ final class AssetReceiveInteractor: AnyCancellableCleaning { iconAppearance: appearanceFacade.selectedIconAppearance ) - let logoInfo = QRLogoInfo( + let logoInfo = IconInfo( size: .qrLogoSize, type: qrLogoType ) diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift index efddcd52ea..36ab842b98 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewFactory.swift @@ -15,7 +15,10 @@ struct AssetReceiveViewFactory { let operationQueue = OperationManagerFacade.sharedDefaultQueue + let imageRetreiveOperationFactory = KingfisherIconRetrieveOperationFactory(operationQueue: operationQueue) + let qrCodeFactory = QRCodeWithLogoFactory( + iconRetrievingFactory: imageRetreiveOperationFactory, operationQueue: operationQueue, callbackQueue: .main, logger: Logger.shared diff --git a/novawallet/Modules/AssetReceive/Model/AssetIconURLFactory.swift b/novawallet/Modules/AssetReceive/Model/AssetIconURLFactory.swift index 6573ee28d4..3d06a35ce1 100644 --- a/novawallet/Modules/AssetReceive/Model/AssetIconURLFactory.swift +++ b/novawallet/Modules/AssetReceive/Model/AssetIconURLFactory.swift @@ -18,7 +18,7 @@ enum AssetIconURLFactory { static func createQRLogoURL( for iconName: String?, iconAppearance: AppearanceIconsOptions - ) -> QRLogoType? { + ) -> IconType? { guard let iconName else { return nil } switch iconAppearance { From d3ef6f1226cb97e7e1be93e7574bc515dc6409df Mon Sep 17 00:00:00 2001 From: svojsu Date: Sun, 3 Nov 2024 23:40:18 +0200 Subject: [PATCH 11/12] add errors --- novawallet.xcodeproj/project.pbxproj | 4 + .../IconRetrieveOperationFactory.swift | 100 +--------------- ...ngfisherIconRetrieveOperationFactory.swift | 107 ++++++++++++++++++ .../Logo/QRCodeWithLogoFactory.swift | 5 - 4 files changed, 114 insertions(+), 102 deletions(-) create mode 100644 novawallet/Common/IconRetrieve/KingfisherIconRetrieveOperationFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 09b797ae0a..30254a5d5e 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -933,6 +933,7 @@ 2D1C5D282C25ACF200E2DBDD /* CustomNetworkEditInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1C5D272C25ACF200E2DBDD /* CustomNetworkEditInteractor.swift */; }; 2D1C5D2A2C25AD0200E2DBDD /* CustomNetworkPresenterProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1C5D292C25AD0200E2DBDD /* CustomNetworkPresenterProtocols.swift */; }; 2D1D66002CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1D65FF2CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift */; }; + 2D1D66022CD82330009C6C2F /* KingfisherIconRetrieveOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1D66012CD82330009C6C2F /* KingfisherIconRetrieveOperationFactory.swift */; }; 2D2F6F522C50E52D005020EF /* VotingCurveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D2F6F512C50E52D005020EF /* VotingCurveTests.swift */; }; 2D32BE122C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */; }; 2D32BE132C6A49900047F520 /* ExtrinsicAssetConversionFeeInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32BE062C6A49900047F520 /* ExtrinsicAssetConversionFeeInstaller.swift */; }; @@ -6118,6 +6119,7 @@ 2D1C5D272C25ACF200E2DBDD /* CustomNetworkEditInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNetworkEditInteractor.swift; sourceTree = ""; }; 2D1C5D292C25AD0200E2DBDD /* CustomNetworkPresenterProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNetworkPresenterProtocols.swift; sourceTree = ""; }; 2D1D65FF2CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconRetrieveOperationFactory.swift; sourceTree = ""; }; + 2D1D66012CD82330009C6C2F /* KingfisherIconRetrieveOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KingfisherIconRetrieveOperationFactory.swift; sourceTree = ""; }; 2D2F6F512C50E52D005020EF /* VotingCurveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingCurveTests.swift; sourceTree = ""; }; 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtrinsicAssetConversionFeeEstimator.swift; sourceTree = ""; }; 2D32BE062C6A49900047F520 /* ExtrinsicAssetConversionFeeInstaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtrinsicAssetConversionFeeInstaller.swift; sourceTree = ""; }; @@ -12502,6 +12504,7 @@ isa = PBXGroup; children = ( 2D1D65FF2CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift */, + 2D1D66012CD82330009C6C2F /* KingfisherIconRetrieveOperationFactory.swift */, ); path = IconRetrieve; sourceTree = ""; @@ -24927,6 +24930,7 @@ 8887813C28B62B0A00E7290F /* FlexibleSpaceView.swift in Sources */, 0C13D3132A80D06B0054BB6F /* StakingRecommendationValidationFactory.swift in Sources */, 77A0B2ED2A3B7F3300CBF653 /* DAppCollectionViewCell.swift in Sources */, + 2D1D66022CD82330009C6C2F /* KingfisherIconRetrieveOperationFactory.swift in Sources */, 77DB70EE2B709E4300288F26 /* Web3AlertRemoteModel.swift in Sources */, 8471538D2653B29100CB91D8 /* ChangeRewardDestinationViewModelFactory.swift in Sources */, 84BE207825E7D62100B4748C /* ActiveEraInfo.swift in Sources */, diff --git a/novawallet/Common/IconRetrieve/IconRetrieveOperationFactory.swift b/novawallet/Common/IconRetrieve/IconRetrieveOperationFactory.swift index 5463fbb1c5..3ecbc96961 100644 --- a/novawallet/Common/IconRetrieve/IconRetrieveOperationFactory.swift +++ b/novawallet/Common/IconRetrieve/IconRetrieveOperationFactory.swift @@ -12,101 +12,7 @@ protocol IconRetrieveOperationFactoryProtocol { func retrieveImageOperation(using cacheKey: String) -> AsyncClosureOperation } -struct KingfisherIconRetrieveOperationFactory { - let imageManager: KingfisherManager - let operationQueue: OperationQueue - - init( - imageManager: KingfisherManager = KingfisherManager.shared, - operationQueue: OperationQueue - ) { - self.imageManager = imageManager - self.operationQueue = operationQueue - } -} - -// MARK: IconRetrieveOperationFactoryProtocol - -extension KingfisherIconRetrieveOperationFactory: IconRetrieveOperationFactoryProtocol { - func checkCacheOperation(using cacheKey: String) -> Operation_iOS.ClosureOperation { - let cache = imageManager.cache - - return ClosureOperation { - let cachedType = cache.imageCachedType(forKey: cacheKey) - - return cachedType.cached - } - } - - func downloadImageOperation(using iconInfo: IconInfo) -> Operation_iOS.AsyncClosureOperation { - let downloader = imageManager.downloader - - return AsyncClosureOperation { resultClosure in - let scale = UIScreen.main.scale - - let scaledSize = CGSize( - width: iconInfo.size.width * scale, - height: iconInfo.size.height * scale - ) - - guard let url = iconInfo.url else { - resultClosure(.failure(QRCodeFactoryError.logoDownloadError)) - - return - } - - let options: KingfisherOptionsInfo = [ - .processor(ResizingImageProcessor(referenceSize: scaledSize)), - .processor(SVGImageProcessor()) - ] - - downloader.downloadImage(with: url, options: options) { result in - var resultImage: UIImage - - switch result { - case let .success(imageResult) where imageResult.image.cgImage != nil: - resultImage = imageResult.image - default: - resultClosure(.failure(QRCodeFactoryError.logoDownloadError)) - - return - } - - let sizeBeforeProcessing = resultImage.size - - if case .remoteTransparent = iconInfo.type { - resultImage = resultImage.redrawWithBackground( - color: R.color.colorTextPrimaryOnWhite()!, - shape: .circle - ) - } - - if let cacheKey = iconInfo.type?.cacheKey { - KingfisherManager.shared.cache.store( - resultImage, - forKey: cacheKey, - options: KingfisherParsedOptionsInfo(nil) - ) - } - - resultClosure(.success(resultImage)) - } - } - } - - func retrieveImageOperation(using cacheKey: String) -> Operation_iOS.AsyncClosureOperation { - let cache = imageManager.cache - - return AsyncClosureOperation { resultClosure in - cache.retrieveImage(forKey: cacheKey) { result in - if - case let .success(cacheResult) = result, - let image = cacheResult.image { - resultClosure(.success(image)) - } else { - resultClosure(.failure(QRCodeFactoryError.logoRetrievingError)) - } - } - } - } +enum ImageRetrievingError: Error { + case logoDownloadError + case logoRetrievingError } diff --git a/novawallet/Common/IconRetrieve/KingfisherIconRetrieveOperationFactory.swift b/novawallet/Common/IconRetrieve/KingfisherIconRetrieveOperationFactory.swift new file mode 100644 index 0000000000..364d343348 --- /dev/null +++ b/novawallet/Common/IconRetrieve/KingfisherIconRetrieveOperationFactory.swift @@ -0,0 +1,107 @@ +import Foundation +import Kingfisher +import Operation_iOS + +struct KingfisherIconRetrieveOperationFactory { + let imageManager: KingfisherManager + let operationQueue: OperationQueue + + init( + imageManager: KingfisherManager = KingfisherManager.shared, + operationQueue: OperationQueue + ) { + self.imageManager = imageManager + self.operationQueue = operationQueue + } +} + +// MARK: IconRetrieveOperationFactoryProtocol + +extension KingfisherIconRetrieveOperationFactory: IconRetrieveOperationFactoryProtocol { + func checkCacheOperation(using cacheKey: String) -> Operation_iOS.ClosureOperation { + let cache = imageManager.cache + + return ClosureOperation { + let cachedType = cache.imageCachedType(forKey: cacheKey) + + return cachedType.cached + } + } + + func downloadImageOperation(using iconInfo: IconInfo) -> Operation_iOS.AsyncClosureOperation { + let downloader = imageManager.downloader + + return AsyncClosureOperation { resultClosure in + let scale = UIScreen.main.scale + + let scaledSize = CGSize( + width: iconInfo.size.width * scale, + height: iconInfo.size.height * scale + ) + + guard let url = iconInfo.url else { + resultClosure(.failure(ImageRetrievingError.logoDownloadError)) + + return + } + + let processor = SVGImageProcessor() |> ResizingImageProcessor(referenceSize: scaledSize) + + let options: KingfisherOptionsInfo = [.processor(processor)] + + downloader.downloadImage(with: url, options: options) { result in + var resultImage: UIImage + + switch result { + case let .success(imageResult) where imageResult.image.cgImage != nil: + resultImage = imageResult.image + default: + resultClosure(.failure(ImageRetrievingError.logoDownloadError)) + + return + } + + let sizeBeforeProcessing = resultImage.size + + if case .remoteTransparent = iconInfo.type { + resultImage = resultImage.redrawWithBackground( + color: R.color.colorTextPrimaryOnWhite()!, + shape: .circle + ) + } + + if let cacheKey = iconInfo.type?.cacheKey { + let cacheOptions: KingfisherOptionsInfo = [ + .cacheSerializer(RemoteImageSerializer.shared), + .cacheOriginalImage, + .diskCacheExpiration(.days(1)) + ] + + KingfisherManager.shared.cache.store( + resultImage, + forKey: cacheKey, + options: KingfisherParsedOptionsInfo(cacheOptions) + ) + } + + resultClosure(.success(resultImage)) + } + } + } + + func retrieveImageOperation(using cacheKey: String) -> Operation_iOS.AsyncClosureOperation { + let cache = imageManager.cache + + return AsyncClosureOperation { resultClosure in + cache.retrieveImage(forKey: cacheKey) { result in + if + case let .success(cacheResult) = result, + let image = cacheResult.image { + resultClosure(.success(image)) + } else { + resultClosure(.failure(ImageRetrievingError.logoRetrievingError)) + } + } + } + } +} diff --git a/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift b/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift index 397bc127be..34abd458fc 100644 --- a/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift +++ b/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift @@ -2,11 +2,6 @@ import Foundation import Kingfisher import Operation_iOS -enum QRCodeFactoryError: Error { - case logoDownloadError - case logoRetrievingError -} - protocol QRCodeWithLogoFactoryProtocol { func createQRCode( with payload: Data, From de333d35f356a9e2c54c160b788e38c5ac6930fc Mon Sep 17 00:00:00 2001 From: svojsu Date: Mon, 4 Nov 2024 17:43:22 +0200 Subject: [PATCH 12/12] review fixes --- novawallet.xcodeproj/project.pbxproj | 12 ++-- .../Extension/UIKit/RoundedView+Styles.swift | 1 + ...ngfisherIconRetrieveOperationFactory.swift | 16 ++++-- .../Logo/QRCodeWithLogoFactory.swift | 13 ++--- .../Logo/QRWithLogoCreationOperation.swift | 40 +++++++------ .../QRWithLogoDisplayView.swift} | 22 +++++--- .../QRCreation/NoLogo/QRDisplayView.swift | 56 +++++++++++++++++++ .../ViewModel/RemoteImageViewModel.swift | 7 +-- .../AssetReceive/AssetReceiveViewLayout.swift | 2 +- .../Nft/ViewModel/NftImageViewModel.swift | 7 +-- .../ParitySignerTxQrViewController.swift | 2 +- 11 files changed, 122 insertions(+), 56 deletions(-) rename novawallet/Common/QRCreation/{QRDisplayView.swift => Logo/QRWithLogoDisplayView.swift} (82%) create mode 100644 novawallet/Common/QRCreation/NoLogo/QRDisplayView.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 30254a5d5e..2aa23f82df 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -934,6 +934,7 @@ 2D1C5D2A2C25AD0200E2DBDD /* CustomNetworkPresenterProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1C5D292C25AD0200E2DBDD /* CustomNetworkPresenterProtocols.swift */; }; 2D1D66002CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1D65FF2CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift */; }; 2D1D66022CD82330009C6C2F /* KingfisherIconRetrieveOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1D66012CD82330009C6C2F /* KingfisherIconRetrieveOperationFactory.swift */; }; + 2D1D66062CD92209009C6C2F /* QRDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1D66052CD92209009C6C2F /* QRDisplayView.swift */; }; 2D2F6F522C50E52D005020EF /* VotingCurveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D2F6F512C50E52D005020EF /* VotingCurveTests.swift */; }; 2D32BE122C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */; }; 2D32BE132C6A49900047F520 /* ExtrinsicAssetConversionFeeInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32BE062C6A49900047F520 /* ExtrinsicAssetConversionFeeInstaller.swift */; }; @@ -3509,7 +3510,7 @@ 84BB3CF8267D276D00676FFE /* CrowdloanTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB3CF7267D276D00676FFE /* CrowdloanTableViewCell.swift */; }; 84BB3D00267D364D00676FFE /* CompoundOperationWrapper+Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB3CFF267D364D00676FFE /* CompoundOperationWrapper+Dependency.swift */; }; 84BC703F289DBE6C008A9758 /* QRWithLogoCreationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC703E289DBE6C008A9758 /* QRWithLogoCreationOperation.swift */; }; - 84BC7041289DBF62008A9758 /* QRDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC7040289DBF62008A9758 /* QRDisplayView.swift */; }; + 84BC7041289DBF62008A9758 /* QRWithLogoDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC7040289DBF62008A9758 /* QRWithLogoDisplayView.swift */; }; 84BC7043289EEF85008A9758 /* NoSigningSupportWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC7042289EEF85008A9758 /* NoSigningSupportWrapper.swift */; }; 84BC7045289EFF44008A9758 /* TransactionDisplayCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC7044289EFF44008A9758 /* TransactionDisplayCode.swift */; }; 84BC7047289EFFFA008A9758 /* ChainWalletDisplayAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BC7046289EFFFA008A9758 /* ChainWalletDisplayAddress.swift */; }; @@ -6120,6 +6121,7 @@ 2D1C5D292C25AD0200E2DBDD /* CustomNetworkPresenterProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNetworkPresenterProtocols.swift; sourceTree = ""; }; 2D1D65FF2CD80A4D009C6C2F /* IconRetrieveOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconRetrieveOperationFactory.swift; sourceTree = ""; }; 2D1D66012CD82330009C6C2F /* KingfisherIconRetrieveOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KingfisherIconRetrieveOperationFactory.swift; sourceTree = ""; }; + 2D1D66052CD92209009C6C2F /* QRDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRDisplayView.swift; sourceTree = ""; }; 2D2F6F512C50E52D005020EF /* VotingCurveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingCurveTests.swift; sourceTree = ""; }; 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtrinsicAssetConversionFeeEstimator.swift; sourceTree = ""; }; 2D32BE062C6A49900047F520 /* ExtrinsicAssetConversionFeeInstaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtrinsicAssetConversionFeeInstaller.swift; sourceTree = ""; }; @@ -8641,7 +8643,7 @@ 84BB3CF7267D276D00676FFE /* CrowdloanTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrowdloanTableViewCell.swift; sourceTree = ""; }; 84BB3CFF267D364D00676FFE /* CompoundOperationWrapper+Dependency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompoundOperationWrapper+Dependency.swift"; sourceTree = ""; }; 84BC703E289DBE6C008A9758 /* QRWithLogoCreationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRWithLogoCreationOperation.swift; sourceTree = ""; }; - 84BC7040289DBF62008A9758 /* QRDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRDisplayView.swift; sourceTree = ""; }; + 84BC7040289DBF62008A9758 /* QRWithLogoDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRWithLogoDisplayView.swift; sourceTree = ""; }; 84BC7042289EEF85008A9758 /* NoSigningSupportWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoSigningSupportWrapper.swift; sourceTree = ""; }; 84BC7044289EFF44008A9758 /* TransactionDisplayCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDisplayCode.swift; sourceTree = ""; }; 84BC7046289EFFFA008A9758 /* ChainWalletDisplayAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainWalletDisplayAddress.swift; sourceTree = ""; }; @@ -12932,6 +12934,7 @@ 2DBB288A2CD42B5100DFA0AD /* Logo */ = { isa = PBXGroup; children = ( + 84BC7040289DBF62008A9758 /* QRWithLogoDisplayView.swift */, 84BC703E289DBE6C008A9758 /* QRWithLogoCreationOperation.swift */, 2DBB28842CD2811200DFA0AD /* QRCodeWithLogoFactory.swift */, 2DBB288E2CD42E8D00DFA0AD /* IconType.swift */, @@ -12945,6 +12948,7 @@ children = ( 2DBB28882CD42ACB00DFA0AD /* QRCreationOperation.swift */, 2DBB288C2CD42C5200DFA0AD /* QRCreationOperationFactory.swift */, + 2D1D66052CD92209009C6C2F /* QRDisplayView.swift */, ); path = NoLogo; sourceTree = ""; @@ -19841,7 +19845,6 @@ children = ( 2DBB288A2CD42B5100DFA0AD /* Logo */, 2DBB288B2CD42B6800DFA0AD /* NoLogo */, - 84BC7040289DBF62008A9758 /* QRDisplayView.swift */, 2D442CF22CD103ED00A0380F /* BarcodeCreationError.swift */, ); path = QRCreation; @@ -26668,7 +26671,7 @@ 77A6F5CB2A30BD71004AFD1A /* BaseKiltTransferAssetRecipientRepository.swift in Sources */, 8442003628EA9DF100C49C4A /* VoteViewFactory.swift in Sources */, 770F57882A8A2CE0005FD7C1 /* StakingSelectPoolViewStyles.swift in Sources */, - 84BC7041289DBF62008A9758 /* QRDisplayView.swift in Sources */, + 84BC7041289DBF62008A9758 /* QRWithLogoDisplayView.swift in Sources */, 2AC7BC7E2731604C001D99B0 /* ChainAccountChanged.swift in Sources */, 845B07F329159C15005785D3 /* Democracy+CodingPath.swift in Sources */, 0CD846152BDA525B0026B5CA /* CloudBackupMessageSheetViewFactory.swift in Sources */, @@ -28503,6 +28506,7 @@ 84D184EA2A04D9980060C1BD /* StackStatusCell.swift in Sources */, 21B297239CC294307EF20B58 /* ParaStkYieldBoostSetupInteractor.swift in Sources */, 0CCE25212A44306200286709 /* TransactionHistoryPhishingFilter.swift in Sources */, + 2D1D66062CD92209009C6C2F /* QRDisplayView.swift in Sources */, 77A6F5C62A2F17BE004AFD1A /* SendAssetOperationPresenter.swift in Sources */, 0C495B902B4290A900B8D339 /* Proxy+Call.swift in Sources */, 846DA55B2A20A56D006CD6C1 /* OffchainMultistakingUpdateService.swift in Sources */, diff --git a/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift b/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift index 18eddf59be..3972d57798 100644 --- a/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift +++ b/novawallet/Common/Extension/UIKit/RoundedView+Styles.swift @@ -27,6 +27,7 @@ extension RoundedView { highlightedStrokeColor = .clear shadowOpacity = .zero fillColor = .clear + highlightedFillColor = R.color.colorCellBackgroundPressed()! } func applyBorderBackgroundStyle() { diff --git a/novawallet/Common/IconRetrieve/KingfisherIconRetrieveOperationFactory.swift b/novawallet/Common/IconRetrieve/KingfisherIconRetrieveOperationFactory.swift index 364d343348..afffceeb08 100644 --- a/novawallet/Common/IconRetrieve/KingfisherIconRetrieveOperationFactory.swift +++ b/novawallet/Common/IconRetrieve/KingfisherIconRetrieveOperationFactory.swift @@ -71,13 +71,9 @@ extension KingfisherIconRetrieveOperationFactory: IconRetrieveOperationFactoryPr } if let cacheKey = iconInfo.type?.cacheKey { - let cacheOptions: KingfisherOptionsInfo = [ - .cacheSerializer(RemoteImageSerializer.shared), - .cacheOriginalImage, - .diskCacheExpiration(.days(1)) - ] + let cacheOptions = KingfisherOptionsInfo.cacheOptions - KingfisherManager.shared.cache.store( + imageManager.cache.store( resultImage, forKey: cacheKey, options: KingfisherParsedOptionsInfo(cacheOptions) @@ -105,3 +101,11 @@ extension KingfisherIconRetrieveOperationFactory: IconRetrieveOperationFactoryPr } } } + +extension KingfisherOptionsInfo { + static let cacheOptions: KingfisherOptionsInfo = [ + .cacheSerializer(RemoteImageSerializer.shared), + .cacheOriginalImage, + .diskCacheExpiration(.days(1)) + ] +} diff --git a/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift b/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift index 34abd458fc..0cf716db1b 100644 --- a/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift +++ b/novawallet/Common/QRCreation/Logo/QRCodeWithLogoFactory.swift @@ -63,7 +63,7 @@ extension QRCodeWithLogoFactory: QRCodeWithLogoFactoryProtocol { novawallet.execute( wrapper: wrapper, inOperationQueue: operationQueue, - runningCallbackIn: .main, + runningCallbackIn: callbackQueue, callbackClosure: completion ) } @@ -238,10 +238,7 @@ private extension QRCodeWithLogoFactory { qrImageWrapper = qrImageWrapper.insertingHead(operations: [logoOperation]) } - let resultMappingWrapper: CompoundOperationWrapper - resultMappingWrapper = OperationCombiningService.compoundNonOptionalWrapper( - operationManager: OperationManager(operationQueue: operationQueue) - ) { + let resultMappingOperation = ClosureOperation { let qrImage = try qrImageWrapper.targetOperation.extractNoCancellableResultData() let result: QRCreationResult = if logoOperation != nil { @@ -250,11 +247,11 @@ private extension QRCodeWithLogoFactory { .noLogo(qrImage) } - return .createWithResult(result) + return result } - resultMappingWrapper.addDependency(wrapper: qrImageWrapper) + resultMappingOperation.addDependency(qrImageWrapper.targetOperation) - return resultMappingWrapper.insertingHead(operations: qrImageWrapper.allOperations) + return qrImageWrapper.insertingTail(operation: resultMappingOperation) } } diff --git a/novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift b/novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift index a4038ad7cb..6817e2587e 100644 --- a/novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift +++ b/novawallet/Common/QRCreation/Logo/QRWithLogoCreationOperation.swift @@ -7,15 +7,18 @@ import QRCode final class QRWithLogoCreationOperation: BaseOperation { let payloadClosure: () throws -> Data let qrSize: CGSize + let scale: CGFloat let logoInfo: IconInfo? init( payload: Data, qrSize: CGSize, + scale: CGFloat = UIScreen.main.scale, logoInfo: IconInfo? = nil ) { payloadClosure = { payload } self.qrSize = qrSize + self.scale = scale self.logoInfo = logoInfo super.init() @@ -24,10 +27,12 @@ final class QRWithLogoCreationOperation: BaseOperation { init( qrSize: CGSize, logoInfo: IconInfo? = nil, + scale: CGFloat = UIScreen.main.scale, payloadClosure: @escaping () throws -> Data ) { self.qrSize = qrSize self.logoInfo = logoInfo + self.scale = scale self.payloadClosure = payloadClosure } @@ -41,23 +46,6 @@ final class QRWithLogoCreationOperation: BaseOperation { qrDoc.design.shape.offPixels = nil qrDoc.design.style.offPixels = nil - let qrCreateImageClosure: () throws -> Void = { [weak self] in - guard let self else { return } - - let scale = UIScreen.main.scale - - let scaledSize = CGSize( - width: qrSize.width * scale, - height: qrSize.height * scale - ) - - guard let cgImage = qrDoc.cgImage(scaledSize) else { - throw BarcodeCreationError.bitmapImageCreationFailed - } - - callback(.success(UIImage(cgImage: cgImage))) - } - switch logoInfo?.type { case let .localColored(image): qrDoc.logoTemplate = QRCode.LogoTemplate.CircleCenter( @@ -73,7 +61,23 @@ final class QRWithLogoCreationOperation: BaseOperation { break } - try qrCreateImageClosure() + try createQRImage(from: qrDoc, callback) + } + + private func createQRImage( + from qrDoc: QRCode.Document, + _ callback: @escaping (Result) -> Void + ) throws { + let scaledSize = CGSize( + width: qrSize.width * scale, + height: qrSize.height * scale + ) + + guard let cgImage = qrDoc.cgImage(scaledSize) else { + throw BarcodeCreationError.bitmapImageCreationFailed + } + + callback(.success(UIImage(cgImage: cgImage))) } } diff --git a/novawallet/Common/QRCreation/QRDisplayView.swift b/novawallet/Common/QRCreation/Logo/QRWithLogoDisplayView.swift similarity index 82% rename from novawallet/Common/QRCreation/QRDisplayView.swift rename to novawallet/Common/QRCreation/Logo/QRWithLogoDisplayView.swift index 64230065b9..a6a59a25ad 100644 --- a/novawallet/Common/QRCreation/QRDisplayView.swift +++ b/novawallet/Common/QRCreation/Logo/QRWithLogoDisplayView.swift @@ -1,7 +1,7 @@ import UIKit import SoraUI -final class QRDisplayView: UIView { +final class QRWithLogoDisplayView: UIView { let backgroundView: RoundedView = { let view = RoundedView() view.fillColor = .white @@ -10,9 +10,7 @@ final class QRDisplayView: UIView { }() let noLogoQRImageView = UIImageView() - let fullQRImageView: UIImageView = .create { view in - view.alpha = 0 - } + let fullQRImageView = UIImageView() var viewModel: QRCodeWithLogoFactory.QRCreationResult? @@ -36,6 +34,14 @@ final class QRDisplayView: UIView { } } + private let appearAnimator: ViewAnimatorProtocol = FadeAnimator( + from: 0.0, + to: 1.0, + duration: 0.3, + delay: 0.0, + options: [.curveLinear] + ) + override init(frame: CGRect) { super.init(frame: frame) @@ -56,12 +62,12 @@ final class QRDisplayView: UIView { case let .full(image) where self.viewModel != nil && self.viewModel != viewModel: fullQRImageView.image = image - UIView.animate(withDuration: 0.3) { - self.fullQRImageView.alpha = 1 - } + appearAnimator.animate( + view: fullQRImageView, + completionBlock: nil + ) case let .full(image): fullQRImageView.image = image - fullQRImageView.alpha = 1 } self.viewModel = viewModel diff --git a/novawallet/Common/QRCreation/NoLogo/QRDisplayView.swift b/novawallet/Common/QRCreation/NoLogo/QRDisplayView.swift new file mode 100644 index 0000000000..b7f16725f5 --- /dev/null +++ b/novawallet/Common/QRCreation/NoLogo/QRDisplayView.swift @@ -0,0 +1,56 @@ +import UIKit +import SoraUI + +final class QRDisplayView: UIView { + let backgroundView: RoundedView = { + let view = RoundedView() + view.fillColor = .white + view.cornerRadius = 24.0 + return view + }() + + let imageView = UIImageView() + + var contentInsets: CGFloat = 8.0 { + didSet { + imageView.snp.updateConstraints { make in + make.edges.equalToSuperview().inset(contentInsets) + } + } + } + + var cornerRadius: CGFloat { + get { + backgroundView.cornerRadius + } + + set { + backgroundView.cornerRadius = newValue + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = .clear + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + addSubview(backgroundView) + backgroundView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + addSubview(imageView) + imageView.snp.makeConstraints { make in + make.edges.equalToSuperview().inset(contentInsets) + } + } +} diff --git a/novawallet/Common/ViewModel/RemoteImageViewModel.swift b/novawallet/Common/ViewModel/RemoteImageViewModel.swift index 57ec6740f0..0eca17d9ad 100644 --- a/novawallet/Common/ViewModel/RemoteImageViewModel.swift +++ b/novawallet/Common/ViewModel/RemoteImageViewModel.swift @@ -32,11 +32,8 @@ extension RemoteImageViewModel: ImageViewModelProtocol { var options: KingfisherOptionsInfo = [ .processor(processor), - .scaleFactor(UIScreen.main.scale), - .cacheSerializer(RemoteImageSerializer.shared), - .cacheOriginalImage, - .diskCacheExpiration(.days(1)) - ] + .scaleFactor(UIScreen.main.scale) + ] + KingfisherOptionsInfo.cacheOptions if let renderingMode = settings.renderingMode { let imageModifier = RenderingModeImageModifier(renderingMode: renderingMode) diff --git a/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift b/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift index 4ed21afe00..f1d8a53e0d 100644 --- a/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift +++ b/novawallet/Modules/AssetReceive/AssetReceiveViewLayout.swift @@ -31,7 +31,7 @@ final class AssetReceiveViewLayout: UIView { view.textAlignment = .center } - let qrView: QRDisplayView = .create { + let qrView: QRWithLogoDisplayView = .create { $0.contentInsets = Constants.qrViewContentInsets $0.backgroundView.shadowOpacity = .zero } diff --git a/novawallet/Modules/Nft/ViewModel/NftImageViewModel.swift b/novawallet/Modules/Nft/ViewModel/NftImageViewModel.swift index 22b70b2fe7..e4ec2a4f93 100644 --- a/novawallet/Modules/Nft/ViewModel/NftImageViewModel.swift +++ b/novawallet/Modules/Nft/ViewModel/NftImageViewModel.swift @@ -56,11 +56,8 @@ final class NftImageViewModel: NftMediaViewModelProtocol { var options: KingfisherOptionsInfo = [ .processor(compoundProcessor), .scaleFactor(UIScreen.main.scale), - .cacheSerializer(RemoteImageSerializer.shared), - .cacheOriginalImage, - .onlyLoadFirstFrame, - .diskCacheExpiration(.days(1)) - ] + .onlyLoadFirstFrame + ] + KingfisherOptionsInfo.cacheOptions if animated { options.append(.transition(.fade(0.25))) diff --git a/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrViewController.swift b/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrViewController.swift index 0cb660c733..6159edcc79 100644 --- a/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrViewController.swift +++ b/novawallet/Modules/ParitySigner/TransactionQr/ParitySignerTxQrViewController.swift @@ -116,7 +116,7 @@ extension ParitySignerTxQrViewController: ParitySignerTxQrViewProtocol { } func didReceiveCode(viewModel: QRImageViewModel) { - rootView.qrView.noLogoQRImageView.bindQr(viewModel: viewModel) + rootView.qrView.imageView.bindQr(viewModel: viewModel) } func didReceiveExpiration(viewModel: ExpirationTimeViewModel) {