@ -0,0 +1,23 @@ |
||||
{ |
||||
"images" : [ |
||||
{ |
||||
"idiom" : "universal", |
||||
"filename" : "discoverDapps.png", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"idiom" : "universal", |
||||
"filename" : "discoverDapps@2x.png", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"idiom" : "universal", |
||||
"filename" : "discoverDapps@3x.png", |
||||
"scale" : "3x" |
||||
} |
||||
], |
||||
"info" : { |
||||
"version" : 1, |
||||
"author" : "xcode" |
||||
} |
||||
} |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 5.4 KiB |
@ -0,0 +1,23 @@ |
||||
{ |
||||
"images" : [ |
||||
{ |
||||
"idiom" : "universal", |
||||
"filename" : "history.png", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"idiom" : "universal", |
||||
"filename" : "history@2x.png", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"idiom" : "universal", |
||||
"filename" : "history@3x.png", |
||||
"scale" : "3x" |
||||
} |
||||
], |
||||
"info" : { |
||||
"version" : 1, |
||||
"author" : "xcode" |
||||
} |
||||
} |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 5.6 KiB |
@ -0,0 +1,23 @@ |
||||
{ |
||||
"images" : [ |
||||
{ |
||||
"idiom" : "universal", |
||||
"filename" : "myDapps.png", |
||||
"scale" : "1x" |
||||
}, |
||||
{ |
||||
"idiom" : "universal", |
||||
"filename" : "myDapps@2x.png", |
||||
"scale" : "2x" |
||||
}, |
||||
{ |
||||
"idiom" : "universal", |
||||
"filename" : "myDapps@3x.png", |
||||
"scale" : "3x" |
||||
} |
||||
], |
||||
"info" : { |
||||
"version" : 1, |
||||
"author" : "xcode" |
||||
} |
||||
} |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 3.3 KiB |
@ -1,429 +0,0 @@ |
||||
// Copyright DApps Platform Inc. All rights reserved. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
import BigInt |
||||
import TrustKeystore |
||||
import RealmSwift |
||||
import WebKit |
||||
|
||||
protocol BrowserCoordinatorDelegate: class { |
||||
func didSentTransaction(transaction: SentTransaction, in coordinator: BrowserCoordinator) |
||||
func didPressCloseButton(in coordinator: BrowserCoordinator) |
||||
} |
||||
|
||||
final class BrowserCoordinator: NSObject, Coordinator { |
||||
private let session: WalletSession |
||||
private let keystore: Keystore |
||||
|
||||
private lazy var bookmarksViewController: BookmarkViewController = { |
||||
let controller = BookmarkViewController(bookmarksStore: bookmarksStore) |
||||
controller.delegate = self |
||||
return controller |
||||
}() |
||||
|
||||
private lazy var historyViewController: HistoryViewController = { |
||||
let controller = HistoryViewController(store: historyStore) |
||||
controller.delegate = self |
||||
return controller |
||||
}() |
||||
|
||||
private lazy var browserViewController: BrowserViewController = { |
||||
let controller = BrowserViewController(account: session.account, config: session.config, server: server) |
||||
controller.delegate = self |
||||
controller.webView.uiDelegate = self |
||||
return controller |
||||
}() |
||||
private let sharedRealm: Realm |
||||
private lazy var bookmarksStore: BookmarksStore = { |
||||
return BookmarksStore(realm: sharedRealm) |
||||
}() |
||||
private lazy var historyStore: HistoryStore = { |
||||
return HistoryStore(realm: sharedRealm) |
||||
}() |
||||
private lazy var preferences: PreferencesController = { |
||||
return PreferencesController() |
||||
}() |
||||
private var urlParser: BrowserURLParser { |
||||
let engine = SearchEngine(rawValue: preferences.get(for: .browserSearchEngine)) ?? .default |
||||
return BrowserURLParser(engine: engine) |
||||
} |
||||
|
||||
private var server: RPCServer { |
||||
return session.config.server |
||||
} |
||||
private var enableToolbar: Bool = true { |
||||
didSet { |
||||
navigationController.isToolbarHidden = !enableToolbar |
||||
} |
||||
} |
||||
|
||||
var coordinators: [Coordinator] = [] |
||||
let navigationController: NavigationController |
||||
|
||||
lazy var rootViewController: MasterBrowserViewController = { |
||||
let controller = MasterBrowserViewController( |
||||
bookmarksViewController: bookmarksViewController, |
||||
historyViewController: historyViewController, |
||||
browserViewController: browserViewController, |
||||
type: .browser |
||||
) |
||||
controller.delegate = self |
||||
return controller |
||||
}() |
||||
|
||||
weak var delegate: BrowserCoordinatorDelegate? |
||||
|
||||
init( |
||||
session: WalletSession, |
||||
keystore: Keystore, |
||||
sharedRealm: Realm |
||||
) { |
||||
self.navigationController = NavigationController(navigationBarClass: BrowserNavigationBar.self, toolbarClass: nil) |
||||
self.session = session |
||||
self.keystore = keystore |
||||
self.sharedRealm = sharedRealm |
||||
} |
||||
|
||||
func start() { |
||||
navigationController.viewControllers = [rootViewController] |
||||
rootViewController.browserViewController.goHome() |
||||
} |
||||
|
||||
@objc func dismiss() { |
||||
navigationController.dismiss(animated: true, completion: nil) |
||||
} |
||||
|
||||
private func executeTransaction(account: Account, action: DappAction, callbackID: Int, transaction: UnconfirmedTransaction, type: ConfirmType, server: RPCServer) { |
||||
let configurator = TransactionConfigurator( |
||||
session: session, |
||||
account: account, |
||||
transaction: transaction |
||||
) |
||||
let coordinator = ConfirmCoordinator( |
||||
session: session, |
||||
configurator: configurator, |
||||
keystore: keystore, |
||||
account: account, |
||||
type: type |
||||
) |
||||
addCoordinator(coordinator) |
||||
coordinator.didCompleted = { [unowned self] result in |
||||
switch result { |
||||
case .success(let type): |
||||
switch type { |
||||
case .signedTransaction(let data): |
||||
let callback = DappCallback(id: callbackID, value: .signTransaction(data)) |
||||
self.rootViewController.browserViewController.notifyFinish(callbackID: callbackID, value: .success(callback)) |
||||
//TODO do we need to do this for a pending transaction? |
||||
// self.delegate?.didSentTransaction(transaction: transaction, in: self) |
||||
case .sentTransaction(let transaction): |
||||
// on send transaction we pass transaction ID only. |
||||
let data = Data(hex: transaction.id) |
||||
let callback = DappCallback(id: callbackID, value: .sentTransaction(data)) |
||||
self.rootViewController.browserViewController.notifyFinish(callbackID: callbackID, value: .success(callback)) |
||||
self.delegate?.didSentTransaction(transaction: transaction, in: self) |
||||
} |
||||
case .failure: |
||||
self.rootViewController.browserViewController.notifyFinish( |
||||
callbackID: callbackID, |
||||
value: .failure(DAppError.cancelled) |
||||
) |
||||
} |
||||
coordinator.didCompleted = nil |
||||
self.removeCoordinator(coordinator) |
||||
self.navigationController.dismiss(animated: true, completion: nil) |
||||
} |
||||
coordinator.start() |
||||
navigationController.present(coordinator.navigationController, animated: true, completion: nil) |
||||
} |
||||
|
||||
func openURL(_ url: URL) { |
||||
rootViewController.browserViewController.goTo(url: url) |
||||
handleToolbar(for: url) |
||||
} |
||||
|
||||
func handleToolbar(for url: URL) { |
||||
let isToolbarHidden = false |
||||
navigationController.isToolbarHidden = isToolbarHidden |
||||
rootViewController.select(viewType: .browser) |
||||
} |
||||
|
||||
func signMessage(with type: SignMessageType, account: Account, callbackID: Int) { |
||||
let coordinator = SignMessageCoordinator( |
||||
navigationController: navigationController, |
||||
keystore: keystore, |
||||
account: account |
||||
) |
||||
coordinator.didComplete = { [weak self] result in |
||||
guard let strongSelf = self else { return } |
||||
switch result { |
||||
case .success(let data): |
||||
let callback: DappCallback |
||||
switch type { |
||||
case .message: |
||||
callback = DappCallback(id: callbackID, value: .signMessage(data)) |
||||
case .personalMessage: |
||||
callback = DappCallback(id: callbackID, value: .signPersonalMessage(data)) |
||||
case .typedMessage: |
||||
callback = DappCallback(id: callbackID, value: .signTypedMessage(data)) |
||||
} |
||||
strongSelf.rootViewController.browserViewController.notifyFinish(callbackID: callbackID, value: .success(callback)) |
||||
case .failure: |
||||
strongSelf.rootViewController.browserViewController.notifyFinish(callbackID: callbackID, value: .failure(DAppError.cancelled)) |
||||
} |
||||
coordinator.didComplete = nil |
||||
strongSelf.removeCoordinator(coordinator) |
||||
} |
||||
coordinator.delegate = self |
||||
addCoordinator(coordinator) |
||||
coordinator.start(with: type) |
||||
} |
||||
|
||||
func presentQRCodeReader() { |
||||
let coordinator = ScanQRCodeCoordinator( |
||||
navigationController: NavigationController() |
||||
) |
||||
coordinator.delegate = self |
||||
addCoordinator(coordinator) |
||||
navigationController.present(coordinator.qrcodeController, animated: true, completion: nil) |
||||
} |
||||
|
||||
private func presentMoreOptions(sender: UIView) { |
||||
let alertController = makeMoreAlertSheet(sender: sender) |
||||
navigationController.present(alertController, animated: true, completion: nil) |
||||
} |
||||
|
||||
private func makeMoreAlertSheet(sender: UIView) -> UIAlertController { |
||||
let alertController = UIAlertController( |
||||
title: nil, |
||||
message: nil, |
||||
preferredStyle: .actionSheet |
||||
) |
||||
alertController.popoverPresentationController?.sourceView = sender |
||||
alertController.popoverPresentationController?.sourceRect = sender.centerRect |
||||
let reloadAction = UIAlertAction(title: R.string.localizable.reload(), style: .default) { [unowned self] _ in |
||||
self.rootViewController.browserViewController.reload() |
||||
} |
||||
let shareAction = UIAlertAction(title: R.string.localizable.share(), style: .default) { [unowned self] _ in |
||||
self.share() |
||||
} |
||||
let cancelAction = UIAlertAction(title: R.string.localizable.cancel(), style: .cancel) { _ in } |
||||
let addBookmarkAction = UIAlertAction(title: R.string.localizable.browserAddbookmarkButtonTitle(), style: .default) { [unowned self] _ in |
||||
self.rootViewController.browserViewController.addBookmark() |
||||
} |
||||
alertController.addAction(reloadAction) |
||||
alertController.addAction(shareAction) |
||||
alertController.addAction(addBookmarkAction) |
||||
alertController.addAction(cancelAction) |
||||
return alertController |
||||
} |
||||
|
||||
private func share() { |
||||
guard let url = rootViewController.browserViewController.webView.url else { return } |
||||
rootViewController.displayLoading() |
||||
rootViewController.showShareActivity(from: UIView(), with: [url]) { [weak self] in |
||||
self?.rootViewController.hideLoading() |
||||
} |
||||
} |
||||
} |
||||
|
||||
extension BrowserCoordinator: BrowserViewControllerDelegate { |
||||
func runAction(action: BrowserAction) { |
||||
switch action { |
||||
case .bookmarks: |
||||
rootViewController.select(viewType: .bookmarks) |
||||
case .addBookmark(let bookmark): |
||||
bookmarksStore.add(bookmarks: [bookmark]) |
||||
case .qrCode: |
||||
presentQRCodeReader() |
||||
case .history: |
||||
rootViewController.select(viewType: .history) |
||||
case .navigationAction(let navAction): |
||||
switch navAction { |
||||
case .home: |
||||
enableToolbar = true |
||||
rootViewController.select(viewType: .browser) |
||||
rootViewController.browserViewController.goHome() |
||||
case .close: |
||||
delegate?.didPressCloseButton(in: self) |
||||
case .more(let sender): |
||||
presentMoreOptions(sender: sender) |
||||
case .enter(let string): |
||||
guard let url = urlParser.url(from: string) else { return } |
||||
openURL(url) |
||||
case .goBack: |
||||
rootViewController.browserViewController.webView.goBack() |
||||
case .beginEditing: |
||||
break |
||||
} |
||||
case .changeURL(let url): |
||||
handleToolbar(for: url) |
||||
} |
||||
} |
||||
|
||||
func didCall(action: DappAction, callbackID: Int) { |
||||
guard case .real(let account) = session.account.type else { |
||||
rootViewController.browserViewController.notifyFinish(callbackID: callbackID, value: .failure(DAppError.cancelled)) |
||||
navigationController.topViewController?.displayError(error: InCoordinatorError.onlyWatchAccount) |
||||
return |
||||
} |
||||
switch action { |
||||
case .signTransaction(let unconfirmedTransaction): |
||||
executeTransaction(account: account, action: action, callbackID: callbackID, transaction: unconfirmedTransaction, type: .signThenSend, server: browserViewController.server) |
||||
case .sendTransaction(let unconfirmedTransaction): |
||||
executeTransaction(account: account, action: action, callbackID: callbackID, transaction: unconfirmedTransaction, type: .signThenSend, server: browserViewController.server) |
||||
case .signMessage(let hexMessage): |
||||
let msg = convertMessageToHex(msg: hexMessage) |
||||
signMessage(with: .message(Data(hex: msg)), account: account, callbackID: callbackID) |
||||
case .signPersonalMessage(let hexMessage): |
||||
let msg = convertMessageToHex(msg: hexMessage) |
||||
signMessage(with: .personalMessage(Data(hex: msg)), account: account, callbackID: callbackID) |
||||
case .signTypedMessage(let typedData): |
||||
signMessage(with: .typedMessage(typedData), account: account, callbackID: callbackID) |
||||
case .unknown: |
||||
break |
||||
} |
||||
} |
||||
|
||||
//allow the message to be passed in as a pure string, if it is then we convert it to hex |
||||
private func convertMessageToHex(msg: String) -> String { |
||||
if msg.hasPrefix("0x") { |
||||
return msg |
||||
} else { |
||||
return msg.hex |
||||
} |
||||
} |
||||
|
||||
func didVisitURL(url: URL, title: String) { |
||||
historyStore.record(url: url, title: title) |
||||
} |
||||
} |
||||
|
||||
extension BrowserCoordinator: SignMessageCoordinatorDelegate { |
||||
func didCancel(in coordinator: SignMessageCoordinator) { |
||||
coordinator.didComplete = nil |
||||
removeCoordinator(coordinator) |
||||
} |
||||
} |
||||
|
||||
extension BrowserCoordinator: ConfirmCoordinatorDelegate { |
||||
func didCancel(in coordinator: ConfirmCoordinator) { |
||||
navigationController.dismiss(animated: true, completion: nil) |
||||
coordinator.didCompleted = nil |
||||
removeCoordinator(coordinator) |
||||
} |
||||
} |
||||
|
||||
extension BrowserCoordinator: ScanQRCodeCoordinatorDelegate { |
||||
func didCancel(in coordinator: ScanQRCodeCoordinator) { |
||||
coordinator.navigationController.dismiss(animated: true, completion: nil) |
||||
removeCoordinator(coordinator) |
||||
} |
||||
|
||||
func didScan(result: String, in coordinator: ScanQRCodeCoordinator) { |
||||
coordinator.navigationController.dismiss(animated: true, completion: nil) |
||||
removeCoordinator(coordinator) |
||||
guard let url = URL(string: result) else { |
||||
return |
||||
} |
||||
openURL(url) |
||||
} |
||||
} |
||||
|
||||
extension BrowserCoordinator: BookmarkViewControllerDelegate { |
||||
func didSelectBookmark(_ bookmark: Bookmark, in viewController: BookmarkViewController) { |
||||
guard let url = bookmark.linkURL else { |
||||
return |
||||
} |
||||
openURL(url) |
||||
} |
||||
} |
||||
|
||||
extension BrowserCoordinator: HistoryViewControllerDelegate { |
||||
func didSelect(history: History, in controller: HistoryViewController) { |
||||
guard let url = history.URL else { |
||||
return |
||||
} |
||||
openURL(url) |
||||
} |
||||
} |
||||
|
||||
extension BrowserCoordinator: WKUIDelegate { |
||||
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { |
||||
if navigationAction.targetFrame == nil { |
||||
browserViewController.webView.load(navigationAction.request) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { |
||||
let alertController = UIAlertController.alertController( |
||||
title: .none, |
||||
message: message, |
||||
style: .alert, |
||||
in: navigationController |
||||
) |
||||
alertController.addAction(UIAlertAction(title: R.string.localizable.oK(), style: .default, handler: { _ in |
||||
completionHandler() |
||||
})) |
||||
navigationController.present(alertController, animated: true, completion: nil) |
||||
} |
||||
|
||||
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { |
||||
let alertController = UIAlertController.alertController( |
||||
title: .none, |
||||
message: message, |
||||
style: .alert, |
||||
in: navigationController |
||||
) |
||||
alertController.addAction(UIAlertAction(title: R.string.localizable.oK(), style: .default, handler: { _ in |
||||
completionHandler(true) |
||||
})) |
||||
alertController.addAction(UIAlertAction(title: R.string.localizable.cancel(), style: .default, handler: { _ in |
||||
completionHandler(false) |
||||
})) |
||||
navigationController.present(alertController, animated: true, completion: nil) |
||||
} |
||||
|
||||
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { |
||||
let alertController = UIAlertController.alertController( |
||||
title: .none, |
||||
message: prompt, |
||||
style: .alert, |
||||
in: navigationController |
||||
) |
||||
alertController.addTextField { (textField) in |
||||
textField.text = defaultText |
||||
} |
||||
alertController.addAction(UIAlertAction(title: R.string.localizable.oK(), style: .default, handler: { _ in |
||||
if let text = alertController.textFields?.first?.text { |
||||
completionHandler(text) |
||||
} else { |
||||
completionHandler(defaultText) |
||||
} |
||||
})) |
||||
alertController.addAction(UIAlertAction(title: R.string.localizable.cancel(), style: .default, handler: { _ in |
||||
completionHandler(nil) |
||||
})) |
||||
navigationController.present(alertController, animated: true, completion: nil) |
||||
} |
||||
} |
||||
|
||||
extension BrowserCoordinator: MasterBrowserViewControllerDelegate { |
||||
func didPressAction(_ action: BrowserToolbarAction) { |
||||
switch action { |
||||
case .view(let viewType): |
||||
switch viewType { |
||||
case .bookmarks: |
||||
break |
||||
case .history: |
||||
break |
||||
case .browser: |
||||
break |
||||
} |
||||
case .qrCode: |
||||
presentQRCodeReader() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,574 @@ |
||||
// Copyright DApps Platform Inc. All rights reserved. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
import BigInt |
||||
import TrustKeystore |
||||
import RealmSwift |
||||
import WebKit |
||||
|
||||
protocol DappBrowserCoordinatorDelegate: class { |
||||
func didSentTransaction(transaction: SentTransaction, inCoordinator coordinator: DappBrowserCoordinator) |
||||
} |
||||
|
||||
final class DappBrowserCoordinator: NSObject, Coordinator { |
||||
private let session: WalletSession |
||||
private let keystore: Keystore |
||||
|
||||
private var browserNavBar: DappBrowserNavigationBar? { |
||||
return navigationController.navigationBar as? DappBrowserNavigationBar |
||||
} |
||||
|
||||
private lazy var historyViewController: BrowserHistoryViewController = { |
||||
let controller = BrowserHistoryViewController(store: historyStore) |
||||
controller.configure(viewModel: HistoriesViewModel(store: historyStore)) |
||||
controller.delegate = self |
||||
return controller |
||||
}() |
||||
|
||||
private lazy var browserViewController: BrowserViewController = { |
||||
let controller = BrowserViewController(account: session.account, config: session.config, server: server) |
||||
controller.delegate = self |
||||
controller.webView.uiDelegate = self |
||||
return controller |
||||
}() |
||||
|
||||
private let sharedRealm: Realm |
||||
private lazy var bookmarksStore: BookmarksStore = { |
||||
return BookmarksStore(realm: sharedRealm) |
||||
}() |
||||
|
||||
private lazy var historyStore: HistoryStore = { |
||||
return HistoryStore(realm: sharedRealm) |
||||
}() |
||||
|
||||
private lazy var preferences: PreferencesController = { |
||||
return PreferencesController() |
||||
}() |
||||
|
||||
private var urlParser: BrowserURLParser { |
||||
let engine = SearchEngine(rawValue: preferences.get(for: .browserSearchEngine)) ?? .default |
||||
return BrowserURLParser(engine: engine) |
||||
} |
||||
|
||||
private var server: RPCServer { |
||||
return session.config.server |
||||
} |
||||
|
||||
private var enableToolbar: Bool = true { |
||||
didSet { |
||||
navigationController.isToolbarHidden = !enableToolbar |
||||
} |
||||
} |
||||
|
||||
var coordinators: [Coordinator] = [] |
||||
let navigationController: NavigationController |
||||
|
||||
lazy var rootViewController: DappsHomeViewController = { |
||||
let vc = DappsHomeViewController(bookmarksStore: bookmarksStore) |
||||
vc.delegate = self |
||||
return vc |
||||
}() |
||||
|
||||
weak var delegate: DappBrowserCoordinatorDelegate? |
||||
|
||||
init( |
||||
session: WalletSession, |
||||
keystore: Keystore, |
||||
sharedRealm: Realm |
||||
) { |
||||
self.navigationController = NavigationController(navigationBarClass: DappBrowserNavigationBar.self, toolbarClass: nil) |
||||
self.session = session |
||||
self.keystore = keystore |
||||
self.sharedRealm = sharedRealm |
||||
|
||||
super.init() |
||||
|
||||
(navigationController.navigationBar as? DappBrowserNavigationBar)?.navigationBarDelegate = self |
||||
} |
||||
|
||||
func start() { |
||||
navigationController.viewControllers = [rootViewController] |
||||
} |
||||
|
||||
@objc func dismiss() { |
||||
navigationController.dismiss(animated: true, completion: nil) |
||||
} |
||||
|
||||
private func executeTransaction(account: Account, action: DappAction, callbackID: Int, transaction: UnconfirmedTransaction, type: ConfirmType, server: RPCServer) { |
||||
let configurator = TransactionConfigurator( |
||||
session: session, |
||||
account: account, |
||||
transaction: transaction |
||||
) |
||||
let coordinator = ConfirmCoordinator( |
||||
session: session, |
||||
configurator: configurator, |
||||
keystore: keystore, |
||||
account: account, |
||||
type: type |
||||
) |
||||
addCoordinator(coordinator) |
||||
coordinator.didCompleted = { [weak self] result in |
||||
guard let strongSelf = self else { return } |
||||
switch result { |
||||
case .success(let type): |
||||
switch type { |
||||
case .signedTransaction(let data): |
||||
let callback = DappCallback(id: callbackID, value: .signTransaction(data)) |
||||
strongSelf.browserViewController.notifyFinish(callbackID: callbackID, value: .success(callback)) |
||||
//TODO do we need to do this for a pending transaction? |
||||
// strongSelf.delegate?.didSentTransaction(transaction: transaction, inCoordinator: strongSelf) |
||||
case .sentTransaction(let transaction): |
||||
// on send transaction we pass transaction ID only. |
||||
let data = Data(hex: transaction.id) |
||||
let callback = DappCallback(id: callbackID, value: .sentTransaction(data)) |
||||
strongSelf.browserViewController.notifyFinish(callbackID: callbackID, value: .success(callback)) |
||||
strongSelf.delegate?.didSentTransaction(transaction: transaction, inCoordinator: strongSelf) |
||||
} |
||||
case .failure: |
||||
strongSelf.browserViewController.notifyFinish( |
||||
callbackID: callbackID, |
||||
value: .failure(DAppError.cancelled) |
||||
) |
||||
} |
||||
coordinator.didCompleted = nil |
||||
strongSelf.removeCoordinator(coordinator) |
||||
strongSelf.navigationController.dismiss(animated: true, completion: nil) |
||||
} |
||||
coordinator.start() |
||||
navigationController.present(coordinator.navigationController, animated: true, completion: nil) |
||||
} |
||||
|
||||
func open(url: URL, browserOnly: Bool = false, animated: Bool = true) { |
||||
//TODO maybe not the best idea to check like this. Because it will always create the browserViewController twice the first time (or maybe it's ok. Just once) |
||||
if navigationController.topViewController != browserViewController { |
||||
browserViewController = BrowserViewController(account: session.account, config: session.config, server: server) |
||||
browserViewController.delegate = self |
||||
browserViewController.webView.uiDelegate = self |
||||
pushOntoNavigationController(viewController: browserViewController, animated: animated) |
||||
} |
||||
browserNavBar?.display(url: url) |
||||
if browserOnly { |
||||
browserNavBar?.makeBrowserOnly() |
||||
} |
||||
browserViewController.goTo(url: url) |
||||
} |
||||
|
||||
func signMessage(with type: SignMessageType, account: Account, callbackID: Int) { |
||||
let coordinator = SignMessageCoordinator( |
||||
navigationController: navigationController, |
||||
keystore: keystore, |
||||
account: account |
||||
) |
||||
coordinator.didComplete = { [weak self] result in |
||||
guard let strongSelf = self else { return } |
||||
switch result { |
||||
case .success(let data): |
||||
let callback: DappCallback |
||||
switch type { |
||||
case .message: |
||||
callback = DappCallback(id: callbackID, value: .signMessage(data)) |
||||
case .personalMessage: |
||||
callback = DappCallback(id: callbackID, value: .signPersonalMessage(data)) |
||||
case .typedMessage: |
||||
callback = DappCallback(id: callbackID, value: .signTypedMessage(data)) |
||||
} |
||||
strongSelf.browserViewController.notifyFinish(callbackID: callbackID, value: .success(callback)) |
||||
case .failure: |
||||
strongSelf.browserViewController.notifyFinish(callbackID: callbackID, value: .failure(DAppError.cancelled)) |
||||
} |
||||
coordinator.didComplete = nil |
||||
strongSelf.removeCoordinator(coordinator) |
||||
} |
||||
coordinator.delegate = self |
||||
addCoordinator(coordinator) |
||||
coordinator.start(with: type) |
||||
} |
||||
|
||||
private func makeMoreAlertSheet(sender: UIView) -> UIAlertController { |
||||
let alertController = UIAlertController( |
||||
title: nil, |
||||
message: nil, |
||||
preferredStyle: .actionSheet |
||||
) |
||||
alertController.popoverPresentationController?.sourceView = sender |
||||
alertController.popoverPresentationController?.sourceRect = sender.centerRect |
||||
|
||||
let reloadAction = UIAlertAction(title: R.string.localizable.reload(), style: .default) { [weak self] _ in |
||||
self?.browserViewController.reload() |
||||
} |
||||
|
||||
let shareAction = UIAlertAction(title: R.string.localizable.share(), style: .default) { [weak self] _ in |
||||
self?.share() |
||||
} |
||||
|
||||
let cancelAction = UIAlertAction(title: R.string.localizable.cancel(), style: .cancel) { _ in } |
||||
let addBookmarkAction = UIAlertAction(title: R.string.localizable.browserAddbookmarkButtonTitle(), style: .default) { [weak self] _ in |
||||
self?.addCurrentPageAsBookmark() |
||||
} |
||||
alertController.addAction(reloadAction) |
||||
alertController.addAction(shareAction) |
||||
alertController.addAction(addBookmarkAction) |
||||
alertController.addAction(cancelAction) |
||||
return alertController |
||||
} |
||||
|
||||
private func share() { |
||||
guard let url = browserViewController.webView.url else { return } |
||||
rootViewController.displayLoading() |
||||
rootViewController.showShareActivity(from: UIView(), with: [url]) { [weak self] in |
||||
self?.rootViewController.hideLoading() |
||||
} |
||||
} |
||||
|
||||
private func openDappInBrowser(_ dapp: Dapp) { |
||||
guard let url = URL(string: dapp.url) else { return } |
||||
open(url: url, animated: false) |
||||
} |
||||
|
||||
private func openDappInBrowser(_ dapp: Bookmark) { |
||||
guard let url = URL(string: dapp.url) else { return } |
||||
open(url: url, animated: false) |
||||
} |
||||
|
||||
private func showDappSuggestions(forText text: String) { |
||||
if let viewController = navigationController.topViewController as? DappsAutoCompletionViewController { |
||||
let hasResults = viewController.filter(withText: text) |
||||
if !hasResults { |
||||
navigationController.popViewController(animated: false) |
||||
} |
||||
} else { |
||||
let viewController = DappsAutoCompletionViewController() |
||||
viewController.delegate = self |
||||
let hasResults = viewController.filter(withText: text) |
||||
if hasResults { |
||||
pushOntoNavigationController(viewController: viewController, animated: false) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private func pushOntoNavigationController(viewController: UIViewController, animated: Bool) { |
||||
viewController.navigationItem.setHidesBackButton(true, animated: false) |
||||
navigationController.pushViewController(viewController, animated: animated) |
||||
} |
||||
|
||||
private func deleteDappFromMyDapp(_ dapp: Bookmark) { |
||||
bookmarksStore.delete(bookmarks: [dapp]) |
||||
refreshDapps() |
||||
} |
||||
|
||||
//TODO can we animate changes better? |
||||
func refreshDapps() { |
||||
rootViewController.configure(viewModel: .init(bookmarksStore: bookmarksStore)) |
||||
for each in navigationController.viewControllers { |
||||
guard let vc = each as? MyDappsViewController else { continue } |
||||
vc.configure(viewModel: .init(bookmarksStore: bookmarksStore)) |
||||
} |
||||
} |
||||
|
||||
private func addCurrentPageAsBookmark() { |
||||
guard let url = browserViewController.webView.url?.absoluteString else { return } |
||||
guard let title = browserViewController.webView.title else { return } |
||||
let bookmark = Bookmark(url: url, title: title) |
||||
bookmarksStore.add(bookmarks: [bookmark]) |
||||
refreshDapps() |
||||
} |
||||
} |
||||
|
||||
extension DappBrowserCoordinator: BrowserViewControllerDelegate { |
||||
func didCall(action: DappAction, callbackID: Int, inBrowserViewController viewController: BrowserViewController) { |
||||
guard case .real(let account) = session.account.type else { |
||||
browserViewController.notifyFinish(callbackID: callbackID, value: .failure(DAppError.cancelled)) |
||||
navigationController.topViewController?.displayError(error: InCoordinatorError.onlyWatchAccount) |
||||
return |
||||
} |
||||
switch action { |
||||
case .signTransaction(let unconfirmedTransaction): |
||||
executeTransaction(account: account, action: action, callbackID: callbackID, transaction: unconfirmedTransaction, type: .signThenSend, server: browserViewController.server) |
||||
case .sendTransaction(let unconfirmedTransaction): |
||||
executeTransaction(account: account, action: action, callbackID: callbackID, transaction: unconfirmedTransaction, type: .signThenSend, server: browserViewController.server) |
||||
case .signMessage(let hexMessage): |
||||
let msg = convertMessageToHex(msg: hexMessage) |
||||
signMessage(with: .message(Data(hex: msg)), account: account, callbackID: callbackID) |
||||
case .signPersonalMessage(let hexMessage): |
||||
let msg = convertMessageToHex(msg: hexMessage) |
||||
signMessage(with: .personalMessage(Data(hex: msg)), account: account, callbackID: callbackID) |
||||
case .signTypedMessage(let typedData): |
||||
signMessage(with: .typedMessage(typedData), account: account, callbackID: callbackID) |
||||
case .unknown: |
||||
break |
||||
} |
||||
} |
||||
|
||||
//allow the message to be passed in as a pure string, if it is then we convert it to hex |
||||
private func convertMessageToHex(msg: String) -> String { |
||||
if msg.hasPrefix("0x") { |
||||
return msg |
||||
} else { |
||||
return msg.hex |
||||
} |
||||
} |
||||
|
||||
func didVisitURL(url: URL, title: String, inBrowserViewController viewController: BrowserViewController) { |
||||
browserNavBar?.display(url: url) |
||||
if let mostRecentUrl = historyStore.histories.first?.url, mostRecentUrl == url.absoluteString { |
||||
} else { |
||||
historyStore.record(url: url, title: title) |
||||
} |
||||
} |
||||
|
||||
func dismissKeyboard(inBrowserViewController viewController: BrowserViewController) { |
||||
browserNavBar?.cancelEditing() |
||||
} |
||||
|
||||
func forceUpdate(url: URL, inBrowserViewController viewController: BrowserViewController) { |
||||
browserNavBar?.display(url: url) |
||||
} |
||||
} |
||||
|
||||
extension DappBrowserCoordinator: SignMessageCoordinatorDelegate { |
||||
func didCancel(in coordinator: SignMessageCoordinator) { |
||||
coordinator.didComplete = nil |
||||
removeCoordinator(coordinator) |
||||
} |
||||
} |
||||
|
||||
extension DappBrowserCoordinator: ConfirmCoordinatorDelegate { |
||||
func didCancel(in coordinator: ConfirmCoordinator) { |
||||
navigationController.dismiss(animated: true, completion: nil) |
||||
coordinator.didCompleted = nil |
||||
removeCoordinator(coordinator) |
||||
} |
||||
} |
||||
|
||||
extension DappBrowserCoordinator: BrowserHistoryViewControllerDelegate { |
||||
func didSelect(history: History, inViewController controller: BrowserHistoryViewController) { |
||||
guard let url = history.URL else { return } |
||||
open(url: url) |
||||
} |
||||
|
||||
func clearHistory(inViewController viewController: BrowserHistoryViewController) { |
||||
historyStore.clearAll() |
||||
viewController.configure(viewModel: HistoriesViewModel(store: historyStore)) |
||||
} |
||||
|
||||
func dismissKeyboard(inViewController viewController: BrowserHistoryViewController) { |
||||
browserNavBar?.cancelEditing() |
||||
} |
||||
} |
||||
|
||||
extension DappBrowserCoordinator: WKUIDelegate { |
||||
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { |
||||
if navigationAction.targetFrame == nil { |
||||
browserViewController.webView.load(navigationAction.request) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { |
||||
let alertController = UIAlertController.alertController( |
||||
title: .none, |
||||
message: message, |
||||
style: .alert, |
||||
in: navigationController |
||||
) |
||||
alertController.addAction(UIAlertAction(title: R.string.localizable.oK(), style: .default, handler: { _ in |
||||
completionHandler() |
||||
})) |
||||
navigationController.present(alertController, animated: true, completion: nil) |
||||
} |
||||
|
||||
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { |
||||
let alertController = UIAlertController.alertController( |
||||
title: .none, |
||||
message: message, |
||||
style: .alert, |
||||
in: navigationController |
||||
) |
||||
alertController.addAction(UIAlertAction(title: R.string.localizable.oK(), style: .default, handler: { _ in |
||||
completionHandler(true) |
||||
})) |
||||
alertController.addAction(UIAlertAction(title: R.string.localizable.cancel(), style: .default, handler: { _ in |
||||
completionHandler(false) |
||||
})) |
||||
navigationController.present(alertController, animated: true, completion: nil) |
||||
} |
||||
|
||||
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) { |
||||
let alertController = UIAlertController.alertController( |
||||
title: .none, |
||||
message: prompt, |
||||
style: .alert, |
||||
in: navigationController |
||||
) |
||||
alertController.addTextField { (textField) in |
||||
textField.text = defaultText |
||||
} |
||||
alertController.addAction(UIAlertAction(title: R.string.localizable.oK(), style: .default, handler: { _ in |
||||
if let text = alertController.textFields?.first?.text { |
||||
completionHandler(text) |
||||
} else { |
||||
completionHandler(defaultText) |
||||
} |
||||
})) |
||||
alertController.addAction(UIAlertAction(title: R.string.localizable.cancel(), style: .default, handler: { _ in |
||||
completionHandler(nil) |
||||
})) |
||||
navigationController.present(alertController, animated: true, completion: nil) |
||||
} |
||||
} |
||||
|
||||
extension DappBrowserCoordinator: DappsHomeViewControllerDelegate { |
||||
func didTapShowMyDappsViewController(inViewController viewController: DappsHomeViewController) { |
||||
let viewController = MyDappsViewController(bookmarksStore: bookmarksStore) |
||||
viewController.configure(viewModel: .init(bookmarksStore: bookmarksStore)) |
||||
viewController.delegate = self |
||||
pushOntoNavigationController(viewController: viewController, animated: true) |
||||
} |
||||
|
||||
func didTapShowBrowserHistoryViewController(inViewController viewController: DappsHomeViewController) { |
||||
pushOntoNavigationController(viewController: historyViewController, animated: true) |
||||
} |
||||
|
||||
func didTapShowDiscoverDappsViewController(inViewController viewController: DappsHomeViewController) { |
||||
let viewController = DiscoverDappsViewController(bookmarksStore: bookmarksStore) |
||||
viewController.configure(viewModel: .init()) |
||||
viewController.delegate = self |
||||
pushOntoNavigationController(viewController: viewController, animated: true) |
||||
} |
||||
|
||||
func didTap(dapp: Bookmark, inViewController viewController: DappsHomeViewController) { |
||||
openDappInBrowser(dapp) |
||||
} |
||||
|
||||
func delete(dapp: Bookmark, inViewController viewController: DappsHomeViewController) { |
||||
deleteDappFromMyDapp(dapp) |
||||
} |
||||
|
||||
func viewControllerWillAppear(_ viewController: DappsHomeViewController) { |
||||
browserNavBar?.enableButtons() |
||||
} |
||||
|
||||
func dismissKeyboard(inViewController viewController: DappsHomeViewController) { |
||||
browserNavBar?.cancelEditing() |
||||
} |
||||
} |
||||
|
||||
extension DappBrowserCoordinator: DiscoverDappsViewControllerDelegate { |
||||
func didTap(dapp: Dapp, inViewController viewController: DiscoverDappsViewController) { |
||||
openDappInBrowser(dapp) |
||||
} |
||||
|
||||
func didAdd(dapp: Dapp, inViewController viewController: DiscoverDappsViewController) { |
||||
refreshDapps() |
||||
} |
||||
|
||||
func didRemove(dapp: Dapp, inViewController viewController: DiscoverDappsViewController) { |
||||
refreshDapps() |
||||
} |
||||
|
||||
func dismissKeyboard(inViewController viewController: DiscoverDappsViewController) { |
||||
browserNavBar?.cancelEditing() |
||||
} |
||||
} |
||||
|
||||
|
||||
extension DappBrowserCoordinator: MyDappsViewControllerDelegate { |
||||
func didTapToEdit(dapp: Bookmark, inViewController viewController: MyDappsViewController) { |
||||
let vc = EditMyDappViewController() |
||||
vc.delegate = self |
||||
vc.configure(viewModel: .init(dapp: dapp)) |
||||
vc.hidesBottomBarWhenPushed = true |
||||
navigationController.present(vc, animated: true) |
||||
} |
||||
|
||||
func didTapToSelect(dapp: Bookmark, inViewController viewController: MyDappsViewController) { |
||||
openDappInBrowser(dapp) |
||||
} |
||||
|
||||
func delete(dapp: Bookmark, inViewController viewController: MyDappsViewController) { |
||||
deleteDappFromMyDapp(dapp) |
||||
viewController.configure(viewModel: .init(bookmarksStore: bookmarksStore)) |
||||
} |
||||
|
||||
func dismissKeyboard(inViewController viewController: MyDappsViewController) { |
||||
browserNavBar?.cancelEditing() |
||||
} |
||||
} |
||||
|
||||
extension DappBrowserCoordinator: DappsAutoCompletionViewControllerDelegate { |
||||
func didTap(dapp: Dapp, inViewController viewController: DappsAutoCompletionViewController) { |
||||
openDappInBrowser(dapp) |
||||
} |
||||
|
||||
func dismissKeyboard(inViewController viewController: DappsAutoCompletionViewController) { |
||||
browserNavBar?.cancelEditing() |
||||
} |
||||
} |
||||
|
||||
extension DappBrowserCoordinator: DappBrowserNavigationBarDelegate { |
||||
|
||||
func didTapBack(inNavigationBar navigationBar: DappBrowserNavigationBar) { |
||||
if let browserVC = navigationController.topViewController as? BrowserViewController, browserVC.webView.canGoBack { |
||||
browserViewController.webView.goBack() |
||||
} else if !(browserNavBar?.isBrowserOnly ?? false) { |
||||
navigationController.popViewController(animated: true) |
||||
if let viewController = navigationController.topViewController as? DappsAutoCompletionViewController { |
||||
browserNavBar?.display(string: viewController.text) |
||||
} else if navigationController.topViewController is DappsHomeViewController { |
||||
browserNavBar?.clearDisplay() |
||||
} |
||||
} |
||||
} |
||||
|
||||
func didTapForward(inNavigationBar navigationBar: DappBrowserNavigationBar) { |
||||
guard let browserVC = navigationController.topViewController as? BrowserViewController, browserVC.webView.canGoForward else { return } |
||||
browserViewController.webView.goForward() |
||||
} |
||||
|
||||
func didTapMore(sender: UIView, inNavigationBar navigationBar: DappBrowserNavigationBar) { |
||||
let alertController = makeMoreAlertSheet(sender: sender) |
||||
navigationController.present(alertController, animated: true, completion: nil) |
||||
} |
||||
|
||||
func didTapClose(inNavigationBar navigationBar: DappBrowserNavigationBar) { |
||||
dismiss() |
||||
} |
||||
|
||||
func didTapHome(inNavigationBar navigationBar: DappBrowserNavigationBar) { |
||||
navigationController.popToRootViewController(animated: true) |
||||
browserNavBar?.clearDisplay() |
||||
} |
||||
|
||||
func didTyped(text: String, inNavigationBar navigationBar: DappBrowserNavigationBar) { |
||||
let text = text.trimmed |
||||
if text.isEmpty { |
||||
if navigationController.topViewController as? DappsAutoCompletionViewController != nil { |
||||
navigationController.popViewController(animated: false) |
||||
} |
||||
} else { |
||||
showDappSuggestions(forText: text) |
||||
} |
||||
} |
||||
|
||||
func didEnter(text: String, inNavigationBar navigationBar: DappBrowserNavigationBar) { |
||||
guard let url = urlParser.url(from: text) else { return } |
||||
open(url: url, animated: false) |
||||
} |
||||
} |
||||
|
||||
extension DappBrowserCoordinator: EditMyDappViewControllerDelegate { |
||||
func didTapSave(dapp: Bookmark, withTitle title: String, url: String, inViewController viewController: EditMyDappViewController) { |
||||
try? sharedRealm.write { |
||||
dapp.title = title |
||||
dapp.url = url |
||||
} |
||||
viewController.dismiss(animated: true) |
||||
refreshDapps() |
||||
} |
||||
|
||||
func didTapCancel(inViewController viewController: EditMyDappViewController) { |
||||
viewController.dismiss(animated: true) |
||||
} |
||||
} |
@ -1,13 +0,0 @@ |
||||
// Copyright DApps Platform Inc. All rights reserved. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
enum BrowserNavigation { |
||||
case goBack |
||||
case more(sender: UIView) |
||||
case close |
||||
case home |
||||
case enter(String) |
||||
case beginEditing |
||||
} |
@ -1,114 +0,0 @@ |
||||
// Copyright DApps Platform Inc. All rights reserved. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
import StatefulViewController |
||||
|
||||
protocol BookmarkViewControllerDelegate: class { |
||||
func didSelectBookmark(_ bookmark: Bookmark, in viewController: BookmarkViewController) |
||||
} |
||||
|
||||
final class BookmarkViewController: UIViewController { |
||||
private let tableView = UITableView(frame: .zero, style: .plain) |
||||
private let bookmarksStore: BookmarksStore |
||||
|
||||
weak var delegate: BookmarkViewControllerDelegate? |
||||
|
||||
lazy var viewModel: BookmarksViewModel = { |
||||
return BookmarksViewModel(bookmarksStore: bookmarksStore) |
||||
}() |
||||
|
||||
init(bookmarksStore: BookmarksStore) { |
||||
self.bookmarksStore = bookmarksStore |
||||
|
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false |
||||
tableView.delegate = self |
||||
tableView.dataSource = self |
||||
tableView.separatorStyle = .singleLine |
||||
tableView.rowHeight = 60 |
||||
tableView.register(R.nib.bookmarkViewCell(), forCellReuseIdentifier: R.nib.bookmarkViewCell.name) |
||||
view.addSubview(tableView) |
||||
emptyView = EmptyView(title: R.string.localizable.browserNoBookmarksLabelTitle()) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor), |
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), |
||||
]) |
||||
} |
||||
|
||||
override func viewWillAppear(_ animated: Bool) { |
||||
super.viewWillAppear(animated) |
||||
setupInitialViewState() |
||||
|
||||
fetch() |
||||
} |
||||
|
||||
func fetch() { |
||||
tableView.reloadData() |
||||
} |
||||
|
||||
func confirmDelete(bookmark: Bookmark, index: IndexPath) { |
||||
confirm(title: R.string.localizable.browserBookmarksConfirmDeleteTitle(), |
||||
okTitle: R.string.localizable.delete(), |
||||
okStyle: .destructive) { [weak self] result in |
||||
guard let strongSelf = self else { return } |
||||
switch result { |
||||
case .success: |
||||
strongSelf.delete(bookmark: bookmark, index: index) |
||||
case .failure: break |
||||
} |
||||
} |
||||
} |
||||
|
||||
func delete(bookmark: Bookmark, index: IndexPath) { |
||||
viewModel.delete(bookmark: bookmark) |
||||
tableView.deleteRows(at: [index], with: .automatic) |
||||
transitionViewStates() |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
} |
||||
|
||||
extension BookmarkViewController: StatefulViewController { |
||||
func hasContent() -> Bool { |
||||
return viewModel.hasBookmarks |
||||
} |
||||
} |
||||
|
||||
extension BookmarkViewController: UITableViewDataSource { |
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
||||
return viewModel.numberOfRows |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
||||
let cell = tableView.dequeueReusableCell(withIdentifier: R.nib.bookmarkViewCell.name, for: indexPath) as! BookmarkViewCell |
||||
cell.viewModel = BookmarkViewModel(bookmark: viewModel.bookmark(for: indexPath)) |
||||
return cell |
||||
} |
||||
} |
||||
|
||||
extension BookmarkViewController: UITableViewDelegate { |
||||
|
||||
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { |
||||
return true |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { |
||||
if editingStyle == .delete { |
||||
let bookmark = viewModel.bookmark(for: indexPath) |
||||
confirmDelete(bookmark: bookmark, index: indexPath) |
||||
} |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
||||
tableView.deselectRow(at: indexPath, animated: true) |
||||
let bookmark = viewModel.bookmark(for: indexPath) |
||||
delegate?.didSelectBookmark(bookmark, in: self) |
||||
} |
||||
} |
@ -0,0 +1,171 @@ |
||||
// |
||||
// Created by James Sangalli on 8/12/18. |
||||
// |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
import StatefulViewController |
||||
|
||||
protocol BrowserHistoryViewControllerDelegate: class { |
||||
func didSelect(history: History, inViewController controller: BrowserHistoryViewController) |
||||
func clearHistory(inViewController viewController: BrowserHistoryViewController) |
||||
func dismissKeyboard(inViewController viewController: BrowserHistoryViewController) |
||||
} |
||||
|
||||
final class BrowserHistoryViewController: UIViewController { |
||||
private let store: HistoryStore |
||||
private let tableView = UITableView(frame: .zero, style: .plain) |
||||
private var viewModel: HistoriesViewModel |
||||
lazy private var headerView = BrowserHistoryViewControllerHeaderView() |
||||
|
||||
weak var delegate: BrowserHistoryViewControllerDelegate? |
||||
|
||||
init(store: HistoryStore) { |
||||
self.store = store |
||||
self.viewModel = HistoriesViewModel(store: store) |
||||
|
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false |
||||
tableView.delegate = self |
||||
tableView.dataSource = self |
||||
tableView.tableHeaderView = headerView |
||||
tableView.separatorStyle = .none |
||||
tableView.register(BrowserHistoryCell.self, forCellReuseIdentifier: BrowserHistoryCell.identifier) |
||||
view.addSubview(tableView) |
||||
emptyView = { |
||||
let emptyView = DappsHomeEmptyView() |
||||
let headerViewModel = DappsHomeHeaderViewViewModel(title: R.string.localizable.dappBrowserBrowserHistory()) |
||||
emptyView.configure(viewModel: .init(headerViewViewModel: headerViewModel, title: R.string.localizable.browserNoHistoryLabelTitle())) |
||||
return emptyView |
||||
}() |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor), |
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), |
||||
]) |
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) |
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
@objc private func keyboardWillShow(notification: NSNotification) { |
||||
if let keyboardEndFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardBeginFrame = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue { |
||||
let keyboardHeight = keyboardEndFrame.size.height |
||||
tableView.contentInset.bottom = keyboardEndFrame.size.height |
||||
} |
||||
} |
||||
|
||||
@objc private func keyboardWillHide(notification: NSNotification) { |
||||
if let keyboardEndFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardBeginFrame = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue { |
||||
tableView.contentInset.bottom = 0 |
||||
} |
||||
} |
||||
|
||||
override func viewWillAppear(_ animated: Bool) { |
||||
super.viewWillAppear(animated) |
||||
setupInitialViewState() |
||||
|
||||
fetch() |
||||
} |
||||
|
||||
func fetch() { |
||||
tableView.reloadData() |
||||
} |
||||
|
||||
func configure(viewModel: HistoriesViewModel) { |
||||
tableView.backgroundColor = Colors.appWhite |
||||
|
||||
resizeTableViewHeader() |
||||
tableView.reloadData() |
||||
endLoading() |
||||
} |
||||
|
||||
private func resizeTableViewHeader() { |
||||
let headerViewModel = DappsHomeHeaderViewViewModel(title: R.string.localizable.dappBrowserBrowserHistory()) |
||||
headerView.delegate = self |
||||
headerView.configure(viewModel: headerViewModel) |
||||
let fittingSize = headerView.systemLayoutSizeFitting(.init(width: tableView.frame.size.width, height: 1000)) |
||||
headerView.frame = .init(x: 0, y: 0, width: 0, height: fittingSize.height) |
||||
tableView.tableHeaderView = headerView |
||||
} |
||||
} |
||||
|
||||
extension BrowserHistoryViewController: StatefulViewController { |
||||
func hasContent() -> Bool { |
||||
return viewModel.hasContent |
||||
} |
||||
} |
||||
|
||||
extension BrowserHistoryViewController: UITableViewDataSource { |
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
||||
return viewModel.numberOfRows |
||||
} |
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int { |
||||
return viewModel.numberOfSections |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
||||
let cell = tableView.dequeueReusableCell(withIdentifier: BrowserHistoryCell.identifier, for: indexPath) as! BrowserHistoryCell |
||||
cell.configure(viewModel: .init(history: viewModel.item(for: indexPath))) |
||||
return cell |
||||
} |
||||
} |
||||
|
||||
extension BrowserHistoryViewController: UITableViewDelegate { |
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
||||
tableView.deselectRow(at: indexPath, animated: true) |
||||
delegate?.dismissKeyboard(inViewController: self) |
||||
let history = viewModel.item(for: indexPath) |
||||
delegate?.didSelect(history: history, inViewController: self) |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { |
||||
return true |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { |
||||
if editingStyle == .delete { |
||||
let history = viewModel.item(for: indexPath) |
||||
confirm( |
||||
title: R.string.localizable.browserHistoryConfirmDeleteTitle(), |
||||
message: history.url, |
||||
okTitle: R.string.localizable.removeButtonTitle(), |
||||
okStyle: .destructive |
||||
) { [weak self] result in |
||||
switch result { |
||||
case .success: |
||||
self?.store.delete(histories: [history]) |
||||
//TODO improve animation |
||||
self?.tableView.reloadData() |
||||
self?.endLoading() |
||||
case .failure: break |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
extension BrowserHistoryViewController: BrowserHistoryViewControllerHeaderViewDelegate { |
||||
func didTapClearAll(inHeaderView headerView: BrowserHistoryViewControllerHeaderView) { |
||||
UIAlertController.alert( |
||||
title: R.string.localizable.dappBrowserClearHistory(), |
||||
message: R.string.localizable.dappBrowserClearHistoryPrompt(), |
||||
alertButtonTitles: [R.string.localizable.clearButtonTitle(), R.string.localizable.cancel()], |
||||
alertButtonStyles: [.destructive, .cancel], |
||||
viewController: self, |
||||
completion: { [weak self] buttonIndex in |
||||
guard let strongSelf = self else { return } |
||||
if buttonIndex == 0 { |
||||
strongSelf.delegate?.clearHistory(inViewController: strongSelf) |
||||
} |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,85 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
protocol DappsAutoCompletionViewControllerDelegate: class { |
||||
func didTap(dapp: Dapp, inViewController viewController: DappsAutoCompletionViewController) |
||||
func dismissKeyboard(inViewController viewController: DappsAutoCompletionViewController) |
||||
} |
||||
|
||||
class DappsAutoCompletionViewController: UIViewController { |
||||
private var viewModel: DappsAutoCompletionViewControllerViewModel |
||||
private let tableView = UITableView(frame: .zero, style: .plain) |
||||
weak var delegate: DappsAutoCompletionViewControllerDelegate? |
||||
var text: String { |
||||
return viewModel.keyword |
||||
} |
||||
|
||||
init() { |
||||
self.viewModel = .init() |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
tableView.register(DappsAutoCompletionCell.self, forCellReuseIdentifier: DappsAutoCompletionCell.identifier) |
||||
tableView.translatesAutoresizingMaskIntoConstraints = false |
||||
tableView.delegate = self |
||||
tableView.dataSource = self |
||||
tableView.separatorStyle = .none |
||||
tableView.backgroundColor = Colors.appBackground |
||||
view.addSubview(tableView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor), |
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), |
||||
]) |
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) |
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
@objc private func keyboardWillShow(notification: NSNotification) { |
||||
if let keyboardEndFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardBeginFrame = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue { |
||||
let keyboardHeight = keyboardEndFrame.size.height |
||||
tableView.contentInset.bottom = keyboardEndFrame.size.height |
||||
} |
||||
} |
||||
|
||||
@objc private func keyboardWillHide(notification: NSNotification) { |
||||
if let keyboardEndFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardBeginFrame = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue { |
||||
tableView.contentInset.bottom = 0 |
||||
} |
||||
} |
||||
|
||||
func filter(withText text: String) -> Bool { |
||||
viewModel.keyword = text |
||||
tableView.reloadData() |
||||
return viewModel.dappSuggestionsCount > 0 |
||||
} |
||||
} |
||||
|
||||
extension DappsAutoCompletionViewController: UITableViewDataSource { |
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
||||
let cell = tableView.dequeueReusableCell(withIdentifier: DappsAutoCompletionCell.identifier, for: indexPath) as! DappsAutoCompletionCell |
||||
let dapp = viewModel.dappSuggestions[indexPath.row] |
||||
cell.configure(viewModel: .init(dapp: dapp, keyword: viewModel.keyword)) |
||||
return cell |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
||||
return viewModel.dappSuggestionsCount |
||||
} |
||||
} |
||||
|
||||
extension DappsAutoCompletionViewController: UITableViewDelegate { |
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
||||
tableView.deselectRow(at: indexPath, animated: true) |
||||
delegate?.dismissKeyboard(inViewController: self) |
||||
let dapp = viewModel.dappSuggestions[indexPath.row] |
||||
delegate?.didTap(dapp: dapp, inViewController: self) |
||||
} |
||||
} |
@ -0,0 +1,192 @@ |
||||
// |
||||
// Created by James Sangalli on 8/12/18. |
||||
// |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
protocol DappsHomeViewControllerDelegate: class { |
||||
func didTapShowMyDappsViewController(inViewController viewController: DappsHomeViewController) |
||||
func didTapShowBrowserHistoryViewController(inViewController viewController: DappsHomeViewController) |
||||
func didTapShowDiscoverDappsViewController(inViewController viewController: DappsHomeViewController) |
||||
func didTap(dapp: Bookmark, inViewController viewController: DappsHomeViewController) |
||||
func delete(dapp: Bookmark, inViewController viewController: DappsHomeViewController) |
||||
func viewControllerWillAppear(_ viewController: DappsHomeViewController) |
||||
func dismissKeyboard(inViewController viewController: DappsHomeViewController) |
||||
} |
||||
|
||||
class DappsHomeViewController: UIViewController { |
||||
private static let headerIdentifier = "header" |
||||
|
||||
private var isEditingDapps = false { |
||||
didSet { |
||||
dismissKeyboard() |
||||
if isEditingDapps { |
||||
if !oldValue { |
||||
let vibration = UIImpactFeedbackGenerator() |
||||
vibration.prepare() |
||||
vibration.impactOccurred() |
||||
} |
||||
guard timerToCheckIfStillEditing == nil else { return } |
||||
timerToCheckIfStillEditing = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] timer in |
||||
guard let strongSelf = self else { return } |
||||
if strongSelf.isTopViewController { |
||||
} else { |
||||
strongSelf.isEditingDapps = false |
||||
} |
||||
} |
||||
} else { |
||||
timerToCheckIfStillEditing?.invalidate() |
||||
timerToCheckIfStillEditing = nil |
||||
} |
||||
dappsCollectionView.reloadData() |
||||
} |
||||
} |
||||
private var timerToCheckIfStillEditing: Timer? |
||||
private var viewModel: DappsHomeViewControllerViewModel |
||||
lazy private var dappsCollectionView = { () -> UICollectionView in |
||||
let layout = UICollectionViewFlowLayout() |
||||
let fixedGutter = CGFloat(24) |
||||
let availableWidth = UIScreen.main.bounds.size.width - (2 * fixedGutter) |
||||
let numberOfColumns: CGFloat |
||||
if ScreenChecker().isBigScreen() { |
||||
numberOfColumns = 6 |
||||
} else { |
||||
numberOfColumns = 3 |
||||
} |
||||
let dimension = (availableWidth / numberOfColumns).rounded(.down) |
||||
//Using a sizing cell doesn't get the same reason after we change network. Resorting to hardcoding the width and height difference |
||||
let itemSize = CGSize(width: dimension, height: dimension + 30) |
||||
let additionalGutter = (availableWidth - itemSize.width * numberOfColumns) / (numberOfColumns + 1) |
||||
layout.itemSize = itemSize |
||||
layout.minimumInteritemSpacing = 0 |
||||
layout.sectionInset = .init(top: 0, left: additionalGutter + fixedGutter, bottom: 0, right: additionalGutter + fixedGutter) |
||||
return UICollectionView(frame: .zero, collectionViewLayout: layout) |
||||
}() |
||||
private let bookmarksStore: BookmarksStore |
||||
weak var delegate: DappsHomeViewControllerDelegate? |
||||
|
||||
init(bookmarksStore: BookmarksStore) { |
||||
self.bookmarksStore = bookmarksStore |
||||
self.viewModel = .init(bookmarksStore: bookmarksStore) |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
dappsCollectionView.translatesAutoresizingMaskIntoConstraints = false |
||||
dappsCollectionView.alwaysBounceVertical = true |
||||
dappsCollectionView.register(DappsHomeViewControllerHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: DappsHomeViewController.headerIdentifier) |
||||
dappsCollectionView.register(DappViewCell.self, forCellWithReuseIdentifier: DappViewCell.identifier) |
||||
dappsCollectionView.dataSource = self |
||||
dappsCollectionView.delegate = self |
||||
view.addSubview(dappsCollectionView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
dappsCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
dappsCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
dappsCollectionView.topAnchor.constraint(equalTo: view.topAnchor), |
||||
dappsCollectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), |
||||
]) |
||||
configure(viewModel: viewModel) |
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) |
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
override func viewWillAppear(_ animated: Bool) { |
||||
super.viewWillAppear(animated) |
||||
delegate?.viewControllerWillAppear(self) |
||||
} |
||||
|
||||
@objc private func keyboardWillShow(notification: NSNotification) { |
||||
if let keyboardEndFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardBeginFrame = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue { |
||||
let keyboardHeight = keyboardEndFrame.size.height |
||||
dappsCollectionView.contentInset.bottom = keyboardEndFrame.size.height |
||||
} |
||||
} |
||||
|
||||
@objc private func keyboardWillHide(notification: NSNotification) { |
||||
if let keyboardEndFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardBeginFrame = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue { |
||||
dappsCollectionView.contentInset.bottom = 0 |
||||
} |
||||
} |
||||
|
||||
func configure(viewModel: DappsHomeViewControllerViewModel) { |
||||
self.viewModel = viewModel |
||||
view.backgroundColor = viewModel.backgroundColor |
||||
dappsCollectionView.backgroundColor = viewModel.backgroundColor |
||||
|
||||
dappsCollectionView.reloadData() |
||||
} |
||||
|
||||
@objc private func showMyDappsViewController() { |
||||
dismissKeyboard() |
||||
delegate?.didTapShowMyDappsViewController(inViewController: self) |
||||
} |
||||
|
||||
@objc private func showBrowserHistoryViewController() { |
||||
dismissKeyboard() |
||||
delegate?.didTapShowBrowserHistoryViewController(inViewController: self) |
||||
} |
||||
|
||||
@objc private func showDiscoverPageViewController() { |
||||
dismissKeyboard() |
||||
delegate?.didTapShowDiscoverDappsViewController(inViewController: self) |
||||
} |
||||
|
||||
private func dismissKeyboard() { |
||||
delegate?.dismissKeyboard(inViewController: self) |
||||
} |
||||
} |
||||
|
||||
extension DappsHomeViewController: UICollectionViewDataSource { |
||||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { |
||||
return viewModel.dappsCount |
||||
} |
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { |
||||
let dapp = viewModel.dapp(atIndex: indexPath.item) |
||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DappViewCell.identifier, for: indexPath) as! DappViewCell |
||||
cell.delegate = self |
||||
cell.configure(viewModel: .init(dapp: dapp)) |
||||
cell.isEditing = isEditingDapps |
||||
return cell |
||||
} |
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { |
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: DappsHomeViewController.headerIdentifier, for: indexPath) as! DappsHomeViewControllerHeaderView |
||||
headerView.configure() |
||||
headerView.myDappsButton.addTarget(self, action: #selector(showMyDappsViewController), for: .touchUpInside) |
||||
headerView.discoverDappsButton.addTarget(self, action: #selector(showDiscoverPageViewController), for: .touchUpInside) |
||||
headerView.historyButton.addTarget(self, action: #selector(showBrowserHistoryViewController), for: .touchUpInside) |
||||
return headerView |
||||
} |
||||
} |
||||
|
||||
extension DappsHomeViewController: UICollectionViewDelegateFlowLayout { |
||||
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { |
||||
dappsCollectionView.deselectItem(at: indexPath, animated: true) |
||||
dismissKeyboard() |
||||
let dapp = viewModel.dapp(atIndex: indexPath.item) |
||||
delegate?.didTap(dapp: dapp, inViewController: self) |
||||
} |
||||
|
||||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { |
||||
let headerView = DappsHomeViewControllerHeaderView() |
||||
headerView.configure() |
||||
let size = headerView.systemLayoutSizeFitting(.init(width: collectionView.frame.size.width, height: 1000)) |
||||
return size |
||||
} |
||||
} |
||||
|
||||
extension DappsHomeViewController: DappViewCellDelegate { |
||||
func didTapDelete(dapp: Bookmark, inCell cell: DappViewCell) { |
||||
delegate?.delete(dapp: dapp, inViewController: self) |
||||
} |
||||
|
||||
func didLongPressed(dapp: Bookmark, onCell cell: DappViewCell) { |
||||
isEditingDapps = true |
||||
} |
||||
} |
@ -0,0 +1,174 @@ |
||||
// |
||||
// Created by James Sangalli on 8/12/18. |
||||
// |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
protocol DiscoverDappsViewControllerDelegate: class { |
||||
func didTap(dapp: Dapp, inViewController viewController: DiscoverDappsViewController) |
||||
func didAdd(dapp: Dapp, inViewController viewController: DiscoverDappsViewController) |
||||
func didRemove(dapp: Dapp, inViewController viewController: DiscoverDappsViewController) |
||||
func dismissKeyboard(inViewController viewController: DiscoverDappsViewController) |
||||
} |
||||
|
||||
class DiscoverDappsViewController: UIViewController { |
||||
|
||||
lazy private var headerBoxView = BoxView(view: DappsHomeHeaderView()) |
||||
private let tableView = UITableView(frame: .zero, style: .plain) |
||||
private var viewModel = DiscoverDappsViewControllerViewModel() |
||||
private var bookmarksStore: BookmarksStore |
||||
weak var delegate: DiscoverDappsViewControllerDelegate? |
||||
|
||||
init(bookmarksStore: BookmarksStore) { |
||||
self.bookmarksStore = bookmarksStore |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
tableView.register(DiscoverDappCell.self, forCellReuseIdentifier: DiscoverDappCell.identifier) |
||||
tableView.translatesAutoresizingMaskIntoConstraints = false |
||||
tableView.tableHeaderView = headerBoxView |
||||
tableView.delegate = self |
||||
tableView.dataSource = self |
||||
tableView.separatorStyle = .none |
||||
view.addSubview(tableView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
tableView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor), |
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), |
||||
]) |
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) |
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
@objc private func keyboardWillShow(notification: NSNotification) { |
||||
if let keyboardEndFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardBeginFrame = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue { |
||||
let keyboardHeight = keyboardEndFrame.size.height |
||||
tableView.contentInset.bottom = keyboardEndFrame.size.height |
||||
} |
||||
} |
||||
|
||||
@objc private func keyboardWillHide(notification: NSNotification) { |
||||
if let keyboardEndFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardBeginFrame = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue { |
||||
tableView.contentInset.bottom = 0 |
||||
} |
||||
} |
||||
|
||||
func configure(viewModel: DiscoverDappsViewControllerViewModel) { |
||||
self.viewModel = viewModel |
||||
|
||||
tableView.backgroundColor = viewModel.backgroundColor |
||||
|
||||
resizeTableViewHeader() |
||||
tableView.reloadData() |
||||
} |
||||
|
||||
private func resizeTableViewHeader() { |
||||
let headerViewModel = DappsHomeHeaderViewViewModel(title: R.string.localizable.discoverDappsButtonImageLabel()) |
||||
headerBoxView.view.configure(viewModel: headerViewModel) |
||||
headerBoxView.backgroundColor = headerViewModel.backgroundColor |
||||
headerBoxView.insets = .init(top: 50, left: 0, bottom: 50, right: 0) |
||||
let fittingSize = headerBoxView.systemLayoutSizeFitting(.init(width: tableView.frame.size.width, height: 1000)) |
||||
headerBoxView.frame = .init(x: 0, y: 0, width: 0, height: fittingSize.height) |
||||
tableView.tableHeaderView = headerBoxView |
||||
} |
||||
|
||||
private func dismissKeyboard() { |
||||
delegate?.dismissKeyboard(inViewController: self) |
||||
} |
||||
} |
||||
|
||||
extension DiscoverDappsViewController: UITableViewDataSource { |
||||
func numberOfSections(in tableView: UITableView) -> Int { |
||||
return viewModel.dappCategories.count |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
||||
let cell = tableView.dequeueReusableCell(withIdentifier: DiscoverDappCell.identifier, for: indexPath) as! DiscoverDappCell |
||||
let dapp = viewModel.dappCategories[indexPath.section].dapps[indexPath.row] |
||||
cell.configure(viewModel: .init(bookmarksStore: bookmarksStore, dapp: dapp)) |
||||
cell.delegate = self |
||||
return cell |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
||||
return viewModel.dappCategories[section].dapps.count |
||||
} |
||||
} |
||||
|
||||
extension DiscoverDappsViewController: UITableViewDelegate { |
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
||||
tableView.deselectRow(at: indexPath, animated: true) |
||||
dismissKeyboard() |
||||
let dapp = viewModel.dappCategories[indexPath.section].dapps[indexPath.row] |
||||
delegate?.didTap(dapp: dapp, inViewController: self) |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { |
||||
let title = viewModel.dappCategories[section].name |
||||
return SectionHeaderView(title: title) |
||||
} |
||||
} |
||||
|
||||
extension DiscoverDappsViewController: DiscoverDappCellDelegate { |
||||
func onAdd(dapp: Dapp, inCell cell: DiscoverDappCell) { |
||||
bookmarksStore.add(bookmarks: [.init(url: dapp.url, title: dapp.name)]) |
||||
tableView.reloadData() |
||||
delegate?.didAdd(dapp: dapp, inViewController: self) |
||||
} |
||||
|
||||
func onRemove(dapp: Dapp, inCell cell: DiscoverDappCell) { |
||||
bookmarksStore.delete(bookmarks: [.init(url: dapp.url, title: dapp.name)]) |
||||
tableView.reloadData() |
||||
delegate?.didRemove(dapp: dapp, inViewController: self) |
||||
} |
||||
} |
||||
|
||||
fileprivate class SectionHeaderView: UIView { |
||||
private let label = UILabel() |
||||
|
||||
var title: String { |
||||
didSet { |
||||
update(title: title) |
||||
} |
||||
} |
||||
|
||||
init(title: String) { |
||||
self.title = title |
||||
super.init(frame: .zero) |
||||
|
||||
update(title: title) |
||||
|
||||
addSubview(label) |
||||
label.translatesAutoresizingMaskIntoConstraints = false |
||||
NSLayoutConstraint.activate([ |
||||
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 31), |
||||
label.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||
label.topAnchor.constraint(equalTo: topAnchor, constant: 12), |
||||
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10), |
||||
]) |
||||
|
||||
configure() |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure() { |
||||
backgroundColor = Colors.appWhite |
||||
|
||||
label.textColor = UIColor(red: 77, green: 77, blue: 77) |
||||
label.font = Fonts.regular(size: 10) |
||||
} |
||||
|
||||
private func update(title: String) { |
||||
label.text = title.localizedUppercase |
||||
} |
||||
} |
@ -0,0 +1,207 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
protocol EditMyDappViewControllerDelegate: class { |
||||
func didTapSave(dapp: Bookmark, withTitle title: String, url: String, inViewController viewController: EditMyDappViewController) |
||||
func didTapCancel(inViewController viewController: EditMyDappViewController) |
||||
} |
||||
|
||||
class EditMyDappViewController: UIViewController { |
||||
private let roundedBackground = RoundedBackground() |
||||
private let screenTitleLabel = UILabel() |
||||
private let iconImageView = UIImageView() |
||||
//Holder to show the shadow around the image because the UIImageView is clipsToBounds=true |
||||
private let imageHolder = UIView() |
||||
private let titleLabel = UILabel() |
||||
private let titleTextField = UITextField() |
||||
private let urlLabel = UILabel() |
||||
private let urlTextField = UITextField() |
||||
private let cancelButton = UIButton(type: .system) |
||||
private let saveButton = UIButton(type: .system) |
||||
private var viewModel: EditMyDappViewControllerViewModel? |
||||
|
||||
weak var delegate: EditMyDappViewControllerDelegate? |
||||
|
||||
init() { |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
roundedBackground.translatesAutoresizingMaskIntoConstraints = false |
||||
view.addSubview(roundedBackground) |
||||
|
||||
iconImageView.translatesAutoresizingMaskIntoConstraints = false |
||||
imageHolder.addSubview(iconImageView) |
||||
|
||||
titleTextField.delegate = self |
||||
|
||||
urlTextField.delegate = self |
||||
|
||||
let stackView = [ |
||||
UIView.spacer(height: 34), |
||||
screenTitleLabel, |
||||
UIView.spacer(height: 28), |
||||
imageHolder, |
||||
UIView.spacer(height: 28), |
||||
titleLabel, |
||||
UIView.spacer(height: 7), |
||||
titleTextField, |
||||
UIView.spacer(height: 18), |
||||
urlLabel, |
||||
UIView.spacer(height: 7), |
||||
urlTextField |
||||
].asStackView(axis: .vertical, alignment: .center) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
roundedBackground.addSubview(stackView) |
||||
|
||||
let footerBar = UIView() |
||||
footerBar.translatesAutoresizingMaskIntoConstraints = false |
||||
roundedBackground.addSubview(footerBar) |
||||
|
||||
let buttonsHeight = Metrics.greenButtonHeight |
||||
saveButton.translatesAutoresizingMaskIntoConstraints = false |
||||
saveButton.setContentCompressionResistancePriority(.required, for: .horizontal) |
||||
saveButton.setContentHuggingPriority(.defaultLow, for: .horizontal) |
||||
saveButton.addTarget(self, action: #selector(save), for: .touchUpInside) |
||||
footerBar.addSubview(saveButton) |
||||
|
||||
cancelButton.addTarget(self, action: #selector(cancel), for: .touchUpInside) |
||||
cancelButton.translatesAutoresizingMaskIntoConstraints = false |
||||
view.addSubview(cancelButton) |
||||
|
||||
let marginToHideBottomRoundedCorners = CGFloat(30) |
||||
NSLayoutConstraint.activate([ |
||||
iconImageView.leadingAnchor.constraint(equalTo: imageHolder.leadingAnchor), |
||||
iconImageView.trailingAnchor.constraint(equalTo: imageHolder.trailingAnchor), |
||||
iconImageView.topAnchor.constraint(equalTo: imageHolder.topAnchor), |
||||
iconImageView.bottomAnchor.constraint(equalTo: imageHolder.bottomAnchor), |
||||
|
||||
imageHolder.widthAnchor.constraint(equalToConstant: 80), |
||||
imageHolder.widthAnchor.constraint(equalTo: imageHolder.heightAnchor), |
||||
|
||||
titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor), |
||||
|
||||
urlLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor), |
||||
|
||||
titleTextField.widthAnchor.constraint(equalTo: stackView.widthAnchor), |
||||
|
||||
urlTextField.widthAnchor.constraint(equalTo: stackView.widthAnchor), |
||||
|
||||
footerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
footerBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
//Additional allowance so there's a margin below the buttons for non-iPhone X devices |
||||
footerBar.topAnchor.constraint(equalTo: view.layoutGuide.bottomAnchor, constant: -buttonsHeight - 3), |
||||
footerBar.bottomAnchor.constraint(equalTo: view.bottomAnchor), |
||||
|
||||
saveButton.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor, constant: 15), |
||||
saveButton.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor, constant: -15), |
||||
saveButton.topAnchor.constraint(equalTo: footerBar.topAnchor), |
||||
saveButton.heightAnchor.constraint(equalToConstant: buttonsHeight), |
||||
|
||||
stackView.leadingAnchor.constraint(equalTo: roundedBackground.leadingAnchor, constant: 37), |
||||
stackView.trailingAnchor.constraint(equalTo: roundedBackground.trailingAnchor, constant: -37), |
||||
stackView.topAnchor.constraint(equalTo: roundedBackground.topAnchor), |
||||
|
||||
cancelButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 14), |
||||
cancelButton.topAnchor.constraint(equalTo: view.layoutGuide.topAnchor), |
||||
|
||||
//We don't use createConstraintsWithContainer() because the top rounded corners need to be lower |
||||
roundedBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
roundedBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
roundedBackground.topAnchor.constraint(equalTo: cancelButton.bottomAnchor, constant: 10), |
||||
roundedBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: marginToHideBottomRoundedCorners), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: EditMyDappViewControllerViewModel) { |
||||
self.viewModel = viewModel |
||||
|
||||
view.backgroundColor = viewModel.backgroundColor |
||||
|
||||
imageHolder.layer.shadowColor = viewModel.imageShadowColor.cgColor |
||||
imageHolder.layer.shadowOffset = viewModel.imageShadowOffset |
||||
imageHolder.layer.shadowOpacity = viewModel.imageShadowOpacity |
||||
imageHolder.layer.shadowRadius = viewModel.imageShadowRadius |
||||
|
||||
iconImageView.backgroundColor = viewModel.imageBackgroundColor |
||||
iconImageView.contentMode = .scaleAspectFill |
||||
iconImageView.clipsToBounds = true |
||||
iconImageView.kf.setImage(with: viewModel.imageUrl, placeholder: viewModel.imagePlaceholder) |
||||
|
||||
screenTitleLabel.text = viewModel.screenTitle |
||||
screenTitleLabel.textAlignment = .center |
||||
screenTitleLabel.font = viewModel.screenFont |
||||
|
||||
titleLabel.textColor = viewModel.titleColor |
||||
titleLabel.font = viewModel.titleFont |
||||
titleLabel.text = viewModel.titleText |
||||
|
||||
urlLabel.textColor = viewModel.urlColor |
||||
urlLabel.font = viewModel.urlFont |
||||
urlLabel.text = viewModel.urlText |
||||
|
||||
titleTextField.borderStyle = viewModel.titleTextFieldBorderStyle |
||||
titleTextField.borderWidth = viewModel.titleTextFieldBorderWidth |
||||
titleTextField.borderColor = viewModel.titleTextFieldBorderColor |
||||
titleTextField.cornerRadius = viewModel.titleTextFieldCornerRadius |
||||
titleTextField.font = viewModel.titleTextFieldFont |
||||
titleTextField.returnKeyType = .next |
||||
titleTextField.text = viewModel.titleTextFieldText |
||||
|
||||
urlTextField.borderStyle = viewModel.urlTextFieldBorderStyle |
||||
urlTextField.borderWidth = viewModel.urlTextFieldBorderWidth |
||||
urlTextField.borderColor = viewModel.urlTextFieldBorderColor |
||||
urlTextField.cornerRadius = viewModel.urlTextFieldCornerRadius |
||||
urlTextField.font = viewModel.urlTextFieldFont |
||||
urlTextField.returnKeyType = .done |
||||
urlTextField.text = viewModel.urlTextFieldText |
||||
|
||||
saveButton.setTitleColor(viewModel.saveButtonTitleColor, for: .normal) |
||||
saveButton.backgroundColor = viewModel.saveButtonBackgroundColor |
||||
saveButton.titleLabel?.font = viewModel.saveButtonFont |
||||
saveButton.setTitle(viewModel.saveButtonTitle, for: .normal) |
||||
saveButton.cornerRadius = viewModel.saveButtonCornerRadius |
||||
|
||||
cancelButton.setTitleColor(viewModel.cancelButtonTitleColor, for: .normal) |
||||
cancelButton.titleLabel?.font = viewModel.cancelButtonFont |
||||
cancelButton.setTitle(viewModel.cancelButtonTitle, for: .normal) |
||||
} |
||||
|
||||
override func viewDidLayoutSubviews() { |
||||
super.viewDidLayoutSubviews() |
||||
imageHolder.layer.cornerRadius = imageHolder.frame.size.width / 2 |
||||
iconImageView.layer.cornerRadius = iconImageView.frame.size.width / 2 |
||||
|
||||
imageHolder.layer.shadowPath = UIBezierPath(roundedRect: imageHolder.bounds, cornerRadius: imageHolder.layer.cornerRadius).cgPath |
||||
} |
||||
|
||||
@objc private func save() { |
||||
guard let dapp = viewModel?.dapp else { return } |
||||
guard let url = urlTextField.text?.trimmed else { return } |
||||
guard !url.isEmpty else { return } |
||||
let title = titleTextField.text?.trimmed ?? "" |
||||
delegate?.didTapSave(dapp: dapp, withTitle: title, url: url, inViewController: self) |
||||
} |
||||
|
||||
@objc private func cancel() { |
||||
delegate?.didTapCancel(inViewController: self) |
||||
} |
||||
} |
||||
|
||||
extension EditMyDappViewController: UITextFieldDelegate { |
||||
public func textFieldShouldReturn(_ textField: UITextField) -> Bool { |
||||
switch textField { |
||||
case titleTextField: |
||||
urlTextField.becomeFirstResponder() |
||||
case urlTextField: |
||||
urlTextField.endEditing(true) |
||||
default: |
||||
break |
||||
} |
||||
return true |
||||
} |
||||
} |
@ -1,113 +0,0 @@ |
||||
// Copyright DApps Platform Inc. All rights reserved. |
||||
|
||||
import UIKit |
||||
import StatefulViewController |
||||
|
||||
protocol HistoryViewControllerDelegate: class { |
||||
func didSelect(history: History, in controller: HistoryViewController) |
||||
} |
||||
|
||||
final class HistoryViewController: UIViewController { |
||||
private let store: HistoryStore |
||||
private let tableView = UITableView(frame: .zero, style: .plain) |
||||
private lazy var viewModel: HistoriesViewModel = { |
||||
return HistoriesViewModel(store: store) |
||||
}() |
||||
|
||||
weak var delegate: HistoryViewControllerDelegate? |
||||
|
||||
init(store: HistoryStore) { |
||||
self.store = store |
||||
|
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false |
||||
tableView.delegate = self |
||||
tableView.dataSource = self |
||||
tableView.separatorStyle = .singleLine |
||||
tableView.rowHeight = 60 |
||||
tableView.register(R.nib.bookmarkViewCell(), forCellReuseIdentifier: R.nib.bookmarkViewCell.name) |
||||
view.addSubview(tableView) |
||||
emptyView = EmptyView(title: R.string.localizable.browserNoHistoryLabelTitle()) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor), |
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), |
||||
]) |
||||
} |
||||
|
||||
override func viewDidLoad() { |
||||
super.viewDidLoad() |
||||
|
||||
// Do any additional setup after loading the view. |
||||
} |
||||
|
||||
override func viewWillAppear(_ animated: Bool) { |
||||
super.viewWillAppear(animated) |
||||
setupInitialViewState() |
||||
|
||||
fetch() |
||||
} |
||||
|
||||
func fetch() { |
||||
tableView.reloadData() |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
} |
||||
|
||||
extension HistoryViewController: StatefulViewController { |
||||
func hasContent() -> Bool { |
||||
return viewModel.hasContent |
||||
} |
||||
} |
||||
|
||||
extension HistoryViewController: UITableViewDataSource { |
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
||||
return viewModel.numberOfRows |
||||
} |
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int { |
||||
return viewModel.numberOfSections |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
||||
let cell = tableView.dequeueReusableCell(withIdentifier: R.nib.bookmarkViewCell.name, for: indexPath) as! BookmarkViewCell |
||||
cell.viewModel = HistoryViewModel(history: viewModel.item(for: indexPath)) |
||||
return cell |
||||
} |
||||
} |
||||
|
||||
extension HistoryViewController: UITableViewDelegate { |
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
||||
tableView.deselectRow(at: indexPath, animated: true) |
||||
let history = viewModel.item(for: indexPath) |
||||
delegate?.didSelect(history: history, in: self) |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { |
||||
return true |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { |
||||
if editingStyle == .delete { |
||||
let history = viewModel.item(for: indexPath) |
||||
confirm( |
||||
title: R.string.localizable.browserHistoryConfirmDeleteTitle(), |
||||
okTitle: R.string.localizable.delete(), |
||||
okStyle: .destructive |
||||
) { [weak self] result in |
||||
switch result { |
||||
case .success: |
||||
self?.store.delete(histories: [history]) |
||||
self?.tableView.reloadData() |
||||
case .failure: break |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,136 +0,0 @@ |
||||
// Copyright DApps Platform Inc. All rights reserved. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
protocol MasterBrowserViewControllerDelegate: class { |
||||
func didPressAction(_ action: BrowserToolbarAction) |
||||
} |
||||
|
||||
enum BrowserToolbarAction { |
||||
case view(BookmarksViewType) |
||||
case qrCode |
||||
} |
||||
|
||||
enum BookmarksViewType: Int { |
||||
case browser |
||||
case bookmarks |
||||
case history |
||||
} |
||||
|
||||
final class MasterBrowserViewController: UIViewController { |
||||
private lazy var segmentController: UISegmentedControl = { |
||||
let items = [ |
||||
R.string.localizable.new(), |
||||
R.string.localizable.bookmarks(), |
||||
R.string.localizable.history(), |
||||
] |
||||
let segmentedControl = UISegmentedControl(items: items) |
||||
segmentedControl.addTarget(self, action: #selector(selectionDidChange(_:)), for: .valueChanged) |
||||
segmentedControl.style() |
||||
return segmentedControl |
||||
}() |
||||
|
||||
private lazy var qrcodeButton: UIButton = { |
||||
let button = Button(size: .normal, style: .borderless) |
||||
button.translatesAutoresizingMaskIntoConstraints = false |
||||
button.setImage(R.image.browser_scan()?.withRenderingMode(.alwaysTemplate), for: .normal) |
||||
button.imageView?.tintColor = Colors.appBackground |
||||
button.addTarget(self, action: #selector(qrReader), for: .touchUpInside) |
||||
return button |
||||
}() |
||||
|
||||
private let bookmarksViewController: BookmarkViewController |
||||
private let historyViewController: HistoryViewController |
||||
|
||||
weak var delegate: MasterBrowserViewControllerDelegate? |
||||
let browserViewController: BrowserViewController |
||||
|
||||
init( |
||||
bookmarksViewController: BookmarkViewController, |
||||
historyViewController: HistoryViewController, |
||||
browserViewController: BrowserViewController, |
||||
type: BookmarksViewType |
||||
) { |
||||
self.bookmarksViewController = bookmarksViewController |
||||
self.historyViewController = historyViewController |
||||
self.browserViewController = browserViewController |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
segmentController.selectedSegmentIndex = type.rawValue |
||||
} |
||||
|
||||
override func viewDidLoad() { |
||||
super.viewDidLoad() |
||||
setupView() |
||||
} |
||||
|
||||
override func viewWillAppear(_ animated: Bool) { |
||||
super.viewWillAppear(animated) |
||||
if let navigationController = navigationController, navigationController.isBeingPresented { |
||||
browserViewController.setUpCloseButtonAs(hidden: false) |
||||
} else if isBeingPresented { |
||||
browserViewController.setUpCloseButtonAs(hidden: false) |
||||
} else { |
||||
browserViewController.setUpCloseButtonAs(hidden: true) |
||||
} |
||||
} |
||||
|
||||
func select(viewType: BookmarksViewType) { |
||||
segmentController.selectedSegmentIndex = viewType.rawValue |
||||
updateView() |
||||
} |
||||
|
||||
private func setupView() { |
||||
let items: [UIBarButtonItem] = [ |
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil), |
||||
UIBarButtonItem(customView: segmentController), |
||||
UIBarButtonItem(customView: qrcodeButton), |
||||
UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil), |
||||
UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil), |
||||
] |
||||
toolbarItems = items |
||||
navigationController?.isToolbarHidden = false |
||||
navigationController?.toolbar.isTranslucent = false |
||||
updateView() |
||||
} |
||||
|
||||
private func updateView() { |
||||
if segmentController.selectedSegmentIndex == BookmarksViewType.bookmarks.rawValue { |
||||
remove(asChildViewController: browserViewController) |
||||
remove(asChildViewController: historyViewController) |
||||
add(asChildViewController: bookmarksViewController) |
||||
} else if segmentController.selectedSegmentIndex == BookmarksViewType.history.rawValue { |
||||
remove(asChildViewController: browserViewController) |
||||
remove(asChildViewController: bookmarksViewController) |
||||
add(asChildViewController: historyViewController) |
||||
} else { |
||||
remove(asChildViewController: bookmarksViewController) |
||||
remove(asChildViewController: historyViewController) |
||||
add(asChildViewController: browserViewController) |
||||
} |
||||
} |
||||
|
||||
@objc func selectionDidChange(_ sender: UISegmentedControl) { |
||||
updateView() |
||||
|
||||
guard let viewType = BookmarksViewType(rawValue: sender.selectedSegmentIndex) else { |
||||
return |
||||
} |
||||
delegate?.didPressAction(.view(viewType)) |
||||
} |
||||
|
||||
@objc func qrReader() { |
||||
delegate?.didPressAction(.qrCode) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
} |
||||
|
||||
extension MasterBrowserViewController: Scrollable { |
||||
func scrollOnTop() { |
||||
browserViewController.goHome() |
||||
} |
||||
} |
@ -0,0 +1,165 @@ |
||||
// |
||||
// Created by James Sangalli on 8/12/18. |
||||
// |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
import StatefulViewController |
||||
|
||||
protocol MyDappsViewControllerDelegate: class { |
||||
func didTapToEdit(dapp: Bookmark, inViewController viewController: MyDappsViewController) |
||||
func didTapToSelect(dapp: Bookmark, inViewController viewController: MyDappsViewController) |
||||
func delete(dapp: Bookmark, inViewController viewController: MyDappsViewController) |
||||
func dismissKeyboard(inViewController viewController: MyDappsViewController) |
||||
} |
||||
|
||||
class MyDappsViewController: UIViewController { |
||||
private let tableView = UITableView(frame: .zero, style: .plain) |
||||
lazy private var headerView = MyDappsViewControllerHeaderView() |
||||
private var viewModel: MyDappsViewControllerViewModel |
||||
private var browserNavBar: DappBrowserNavigationBar? { |
||||
return navigationController?.navigationBar as? DappBrowserNavigationBar |
||||
} |
||||
private let bookmarksStore: BookmarksStore |
||||
|
||||
weak var delegate: MyDappsViewControllerDelegate? |
||||
|
||||
init(bookmarksStore: BookmarksStore) { |
||||
self.bookmarksStore = bookmarksStore |
||||
self.viewModel = .init(bookmarksStore: bookmarksStore) |
||||
super.init(nibName: nil, bundle: nil) |
||||
|
||||
tableView.register(MyDappCell.self, forCellReuseIdentifier: MyDappCell.identifier) |
||||
tableView.translatesAutoresizingMaskIntoConstraints = false |
||||
tableView.tableHeaderView = headerView |
||||
tableView.delegate = self |
||||
tableView.dataSource = self |
||||
tableView.separatorStyle = .none |
||||
tableView.backgroundColor = Colors.appBackground |
||||
tableView.allowsSelectionDuringEditing = true |
||||
emptyView = { |
||||
let emptyView = DappsHomeEmptyView() |
||||
let headerViewModel = DappsHomeHeaderViewViewModel(title: R.string.localizable.myDappsButtonImageLabel()) |
||||
emptyView.configure(viewModel: .init(headerViewViewModel: headerViewModel, title: R.string.localizable.dappBrowserMyDappsEmpty())) |
||||
return emptyView |
||||
}() |
||||
view.addSubview(tableView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
tableView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor), |
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), |
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), |
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), |
||||
]) |
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) |
||||
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
@objc private func keyboardWillShow(notification: NSNotification) { |
||||
if let keyboardEndFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardBeginFrame = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue { |
||||
let keyboardHeight = keyboardEndFrame.size.height |
||||
tableView.contentInset.bottom = keyboardEndFrame.size.height |
||||
} |
||||
} |
||||
|
||||
@objc private func keyboardWillHide(notification: NSNotification) { |
||||
if let keyboardEndFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let keyboardBeginFrame = (notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue { |
||||
tableView.contentInset.bottom = 0 |
||||
} |
||||
} |
||||
|
||||
func configure(viewModel: MyDappsViewControllerViewModel) { |
||||
self.viewModel = viewModel |
||||
resizeTableViewHeader() |
||||
tableView.reloadData() |
||||
endLoading() |
||||
} |
||||
|
||||
private func resizeTableViewHeader() { |
||||
headerView.delegate = self |
||||
let headerViewModel = DappsHomeHeaderViewViewModel(title: R.string.localizable.myDappsButtonImageLabel()) |
||||
headerView.configure(viewModel: headerViewModel) |
||||
headerView.backgroundColor = headerViewModel.backgroundColor |
||||
let fittingSize = headerView.systemLayoutSizeFitting(.init(width: tableView.frame.size.width, height: 1000)) |
||||
headerView.frame = .init(x: 0, y: 0, width: 0, height: fittingSize.height) |
||||
tableView.tableHeaderView = headerView |
||||
} |
||||
|
||||
private func dismissKeyboard() { |
||||
delegate?.dismissKeyboard(inViewController: self) |
||||
} |
||||
} |
||||
|
||||
extension MyDappsViewController: StatefulViewController { |
||||
func hasContent() -> Bool { |
||||
return viewModel.hasContent |
||||
} |
||||
} |
||||
|
||||
extension MyDappsViewController: UITableViewDataSource { |
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { |
||||
let cell = tableView.dequeueReusableCell(withIdentifier: MyDappCell.identifier, for: indexPath) as! MyDappCell |
||||
let dapp = viewModel.dapp(atIndex: indexPath.row) |
||||
cell.configure(viewModel: .init(dapp: dapp)) |
||||
return cell |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { |
||||
return viewModel.dappsCount |
||||
} |
||||
} |
||||
|
||||
extension MyDappsViewController: UITableViewDelegate { |
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { |
||||
tableView.deselectRow(at: indexPath, animated: true) |
||||
let dapp = viewModel.dapp(atIndex: indexPath.row) |
||||
if tableView.isEditing { |
||||
delegate?.didTapToEdit(dapp: dapp, inViewController: self) |
||||
} else { |
||||
dismissKeyboard() |
||||
delegate?.didTapToSelect(dapp: dapp, inViewController: self) |
||||
} |
||||
} |
||||
|
||||
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { |
||||
if editingStyle == .delete { |
||||
let dapp = viewModel.dapp(atIndex: indexPath.row) |
||||
confirm( |
||||
title: R.string.localizable.dappBrowserClearMyDapps(), |
||||
message: dapp.title, |
||||
okTitle: R.string.localizable.removeButtonTitle(), |
||||
okStyle: .destructive |
||||
) { [weak self] result in |
||||
switch result { |
||||
case .success: |
||||
guard let strongSelf = self else { return } |
||||
strongSelf.delegate?.delete(dapp: dapp, inViewController: strongSelf) |
||||
if !strongSelf.viewModel.hasContent { |
||||
strongSelf.headerView.exitEditMode() |
||||
} |
||||
case .failure: |
||||
break |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
extension MyDappsViewController: MyDappsViewControllerHeaderViewDelegate { |
||||
func didEnterEditMode(inHeaderView headerView: MyDappsViewControllerHeaderView) { |
||||
//TODO should this be a state case in the nav bar, but with a flag (associated value?) whether to disable the buttons? |
||||
browserNavBar?.disableButtons() |
||||
tableView.setEditing(true, animated: true) |
||||
} |
||||
|
||||
func didExitEditMode(inHeaderView headerView: MyDappsViewControllerHeaderView) { |
||||
//TODO should this be a state case in the nav bar, but with a flag (associated value?) whether to disable the buttons? |
||||
browserNavBar?.enableButtons() |
||||
tableView.setEditing(false, animated: true) |
||||
} |
||||
} |
@ -1,26 +0,0 @@ |
||||
// Copyright DApps Platform Inc. All rights reserved. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct BookmarkViewModel: URLViewModel { |
||||
private let bookmark: Bookmark |
||||
|
||||
init( |
||||
bookmark: Bookmark |
||||
) { |
||||
self.bookmark = bookmark |
||||
} |
||||
|
||||
var urlText: String? { |
||||
return bookmark.linkURL?.absoluteString |
||||
} |
||||
|
||||
var title: String { |
||||
return bookmark.title |
||||
} |
||||
|
||||
var imageURL: URL? { |
||||
return Favicon.get(for: bookmark.linkURL) |
||||
} |
||||
} |
@ -1,31 +0,0 @@ |
||||
// Copyright DApps Platform Inc. All rights reserved. |
||||
|
||||
import Foundation |
||||
import RealmSwift |
||||
|
||||
struct BookmarksViewModel { |
||||
|
||||
private let bookmarksStore: BookmarksStore |
||||
|
||||
init( |
||||
bookmarksStore: BookmarksStore |
||||
) { |
||||
self.bookmarksStore = bookmarksStore |
||||
} |
||||
|
||||
var hasBookmarks: Bool { |
||||
return !bookmarksStore.bookmarks.isEmpty |
||||
} |
||||
|
||||
var numberOfRows: Int { |
||||
return bookmarksStore.bookmarks.count |
||||
} |
||||
|
||||
func bookmark(for indexPath: IndexPath) -> Bookmark { |
||||
return bookmarksStore.bookmarks[indexPath.row] |
||||
} |
||||
|
||||
func delete(bookmark: Bookmark) { |
||||
bookmarksStore.delete(bookmarks: [bookmark]) |
||||
} |
||||
} |
@ -0,0 +1,60 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct BrowserHistoryCellViewModel { |
||||
let history: History |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
|
||||
var imageUrl: URL? { |
||||
return Favicon.get(for: URL(string: history.url)) |
||||
} |
||||
|
||||
var fallbackImage: UIImage? { |
||||
return R.image.launch_icon() |
||||
} |
||||
|
||||
var name: String { |
||||
return history.title |
||||
} |
||||
|
||||
var url: String { |
||||
return history.url |
||||
} |
||||
|
||||
var nameFont: UIFont { |
||||
return Fonts.semibold(size: 12)! |
||||
} |
||||
|
||||
var urlFont: UIFont { |
||||
return Fonts.semibold(size: 10)! |
||||
} |
||||
|
||||
var nameColor: UIColor? { |
||||
return UIColor(red: 77, green: 77, blue: 77) |
||||
} |
||||
|
||||
var urlColor: UIColor? { |
||||
return Colors.appBackground |
||||
} |
||||
|
||||
var imageViewShadowColor: UIColor { |
||||
return .black |
||||
} |
||||
|
||||
var imageViewShadowOffset: CGSize { |
||||
return Metrics.DappsHome.Icon.shadowOffset |
||||
} |
||||
|
||||
var imageViewShadowOpacity: Float { |
||||
return Metrics.DappsHome.Icon.shadowOpacity |
||||
} |
||||
|
||||
var imageViewShadowRadius: CGFloat { |
||||
return Metrics.DappsHome.Icon.shadowRadius |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
|
||||
struct Dapp { |
||||
let name: String |
||||
let description: String |
||||
let url: String |
||||
let cat: String |
||||
} |
@ -0,0 +1,17 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct DappButtonViewModel { |
||||
var font: UIFont? { |
||||
return Fonts.semibold(size: 10) |
||||
} |
||||
|
||||
var textColor: UIColor? { |
||||
return .init(red: 77, green: 77, blue: 77) |
||||
} |
||||
|
||||
let image: UIImage? |
||||
let title: String |
||||
} |
@ -0,0 +1,61 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
class DappViewCellViewModel { |
||||
let dapp: Bookmark |
||||
var imageUrl: URL? |
||||
var title: String { |
||||
return dapp.title |
||||
} |
||||
|
||||
var fallbackImage: UIImage? { |
||||
return R.image.launch_icon() |
||||
} |
||||
|
||||
var domainName: String { |
||||
return URL(string: dapp.url)?.host ?? "" |
||||
} |
||||
|
||||
init(dapp: Bookmark) { |
||||
self.dapp = dapp |
||||
self.imageUrl = Favicon.get(for: URL(string: dapp.url)) |
||||
} |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
|
||||
var imageViewShadowColor: UIColor { |
||||
return .black |
||||
} |
||||
|
||||
var imageViewShadowOffset: CGSize { |
||||
return Metrics.DappsHome.Icon.shadowOffset |
||||
} |
||||
|
||||
var imageViewShadowOpacity: Float { |
||||
return Metrics.DappsHome.Icon.shadowOpacity |
||||
} |
||||
|
||||
var imageViewShadowRadius: CGFloat { |
||||
return Metrics.DappsHome.Icon.shadowRadius |
||||
} |
||||
|
||||
var titleColor: UIColor { |
||||
return UIColor(red: 77, green: 77, blue: 77) |
||||
} |
||||
|
||||
var titleFont: UIFont { |
||||
return Fonts.regular(size: 12)! |
||||
} |
||||
|
||||
var domainNameColor: UIColor { |
||||
return Colors.appBackground |
||||
} |
||||
|
||||
var domainNameFont: UIFont { |
||||
return Fonts.bold(size: 10)! |
||||
} |
||||
} |
@ -0,0 +1,35 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
|
||||
enum Dapps { |
||||
static let masterList = [ |
||||
Dapp(name: "AirSwap", description: "Peer-to-Peer trading on Ethereum", url: "https://airswap.io", cat: "Games"), |
||||
Dapp(name: "Chibi Fighters", description: "Chibi Fighters are fierce little warriors that know no mercy", url: "https://chibifighters.io", cat: "Games"), |
||||
Dapp(name: "CryptoKitties", description: "Collect and breed digital cats!", url: "https://cryptokitties.co", cat: "Misc"), |
||||
Dapp(name: "Multitoken Protocol", description: "Protect Crypto Investments from Volatility", url: "https://multitoken.com", cat: "Misc"), |
||||
] |
||||
|
||||
struct Category { |
||||
let name: String |
||||
var dapps: [Dapp] |
||||
} |
||||
|
||||
static let categorisedDapps: [Category] = { |
||||
var results = [String: Category]() |
||||
for each in masterList { |
||||
let catName = each.cat |
||||
if var cat = results[catName] { |
||||
var dapps = cat.dapps |
||||
dapps.append(each) |
||||
cat.dapps = dapps |
||||
results[catName] = cat |
||||
} else { |
||||
var cat = Category(name: catName, dapps: [each]) |
||||
results[catName] = cat |
||||
} |
||||
} |
||||
//TODO sort categories by hand |
||||
return Array(results.values) |
||||
}() |
||||
} |
@ -0,0 +1,44 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct DappsAutoCompletionCellViewModel { |
||||
let dapp: Dapp |
||||
let keyword: String |
||||
|
||||
var backgroundColor: UIColor { |
||||
return UIColor(red: 244, green: 244, blue: 244) |
||||
} |
||||
|
||||
var name: NSAttributedString { |
||||
let text = NSMutableAttributedString(string: dapp.name) |
||||
text.setAttributes([NSAttributedString.Key.foregroundColor: nameColor], range: .init(location: 0, length: dapp.name.count)) |
||||
if let range = dapp.name.lowercased().range(of: keyword.lowercased()) { |
||||
let location = dapp.name.distance(from: dapp.name.startIndex, to: range.lowerBound) |
||||
let length = keyword.characters.count |
||||
text.setAttributes([NSAttributedString.Key.foregroundColor: Colors.appBackground], range: .init(location: location, length: length)) |
||||
} |
||||
return text |
||||
} |
||||
|
||||
var description: String { |
||||
return dapp.description |
||||
} |
||||
|
||||
var nameFont: UIFont { |
||||
return Fonts.regular(size: 16)! |
||||
} |
||||
|
||||
var descriptionFont: UIFont { |
||||
return Fonts.light(size: 12)! |
||||
} |
||||
|
||||
private var nameColor: UIColor? { |
||||
return UIColor(red: 55, green: 55, blue: 55) |
||||
} |
||||
|
||||
var descriptionColor: UIColor? { |
||||
return UIColor(red: 77, green: 77, blue: 77) |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
|
||||
struct DappsAutoCompletionViewControllerViewModel { |
||||
var dappSuggestions = [Dapp]() |
||||
|
||||
var dappSuggestionsCount: Int { |
||||
return dappSuggestions.count |
||||
} |
||||
|
||||
var keyword: String = "" { |
||||
didSet { |
||||
let lowercased = keyword.lowercased().trimmed |
||||
dappSuggestions = Dapps.masterList.filter { $0.name.lowercased().contains(lowercased) || $0.url.lowercased().contains(lowercased) } |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
|
||||
struct DappsHomeEmptyViewViewModel { |
||||
let headerViewViewModel: DappsHomeHeaderViewViewModel |
||||
let title: String |
||||
} |
@ -0,0 +1,20 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct DappsHomeHeaderViewViewModel { |
||||
var title: String |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
|
||||
var logo: UIImage? { |
||||
return R.image.launch_icon() |
||||
} |
||||
|
||||
var titleFont: UIFont? { |
||||
return Fonts.light(size: 20) |
||||
} |
||||
} |
@ -0,0 +1,38 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct DappsHomeViewControllerHeaderViewViewModel { |
||||
var backgroundColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
|
||||
var title: String { |
||||
return R.string.localizable.dappBrowserTitle() |
||||
} |
||||
|
||||
var myDappsButtonImage: UIImage? { |
||||
return R.image.myDapps() |
||||
} |
||||
|
||||
var myDappsButtonTitle: String { |
||||
return R.string.localizable.myDappsButtonImageLabel() |
||||
} |
||||
|
||||
var discoverButtonImage: UIImage? { |
||||
return R.image.discoverDapps() |
||||
} |
||||
|
||||
var discoverButtonTitle: String { |
||||
return R.string.localizable.discoverDappsButtonImageLabel() |
||||
} |
||||
|
||||
var historyButtonImage: UIImage? { |
||||
return R.image.history() |
||||
} |
||||
|
||||
var historyButtonTitle: String { |
||||
return R.string.localizable.historyButtonImageLabel() |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct DappsHomeViewControllerViewModel { |
||||
var bookmarksStore: BookmarksStore |
||||
|
||||
var dappsCount: Int { |
||||
return bookmarksStore.bookmarks.count |
||||
} |
||||
|
||||
func dapp(atIndex index: Int) -> Bookmark { |
||||
return bookmarksStore.bookmarks[index] |
||||
} |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
} |
@ -0,0 +1,99 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct DiscoverDappCellViewModel { |
||||
let bookmarksStore: BookmarksStore |
||||
let dapp: Dapp |
||||
|
||||
private var containsDapp: Bool { |
||||
//TODO can we not loop? Or at least we can cache this value, no need to be a computed var |
||||
for each in bookmarksStore.bookmarks { |
||||
if each.url == dapp.url { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
var isAddButtonHidden: Bool { |
||||
return containsDapp |
||||
} |
||||
|
||||
var isRemoveButtonHidden: Bool { |
||||
return !containsDapp |
||||
} |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
|
||||
var imageUrl: URL? { |
||||
return Favicon.get(for: URL(string: dapp.url)) |
||||
} |
||||
|
||||
var fallbackImage: UIImage? { |
||||
return R.image.launch_icon() |
||||
} |
||||
|
||||
var name: String { |
||||
return dapp.name |
||||
} |
||||
|
||||
var description: String { |
||||
return dapp.description |
||||
} |
||||
|
||||
var nameFont: UIFont { |
||||
return Fonts.semibold(size: 12)! |
||||
} |
||||
|
||||
var descriptionFont: UIFont { |
||||
return Fonts.regular(size: 10)! |
||||
} |
||||
|
||||
var nameColor: UIColor? { |
||||
return UIColor(red: 77, green: 77, blue: 77) |
||||
} |
||||
|
||||
var descriptionColor: UIColor? { |
||||
return UIColor(red: 77, green: 77, blue: 77) |
||||
} |
||||
|
||||
var addRemoveButtonFont: UIFont { |
||||
return Fonts.semibold(size: 12)! |
||||
} |
||||
|
||||
var addRemoveButtonContentEdgeInsets: UIEdgeInsets { |
||||
return .init(top: 7, left: 14, bottom: 7, right: 14) |
||||
} |
||||
|
||||
var addRemoveButtonBorderColor: UIColor { |
||||
return Colors.appBackground |
||||
} |
||||
|
||||
var addRemoveButtonBorderWidth: CGFloat { |
||||
return 1 |
||||
} |
||||
|
||||
var addRemoveButtonBorderCornerRadius: CGFloat { |
||||
return 9 |
||||
} |
||||
|
||||
var imageViewShadowColor: UIColor { |
||||
return .black |
||||
} |
||||
|
||||
var imageViewShadowOffset: CGSize { |
||||
return Metrics.DappsHome.Icon.shadowOffset |
||||
} |
||||
|
||||
var imageViewShadowOpacity: Float { |
||||
return Metrics.DappsHome.Icon.shadowOpacity |
||||
} |
||||
|
||||
var imageViewShadowRadius: CGFloat { |
||||
return Metrics.DappsHome.Icon.shadowRadius |
||||
} |
||||
} |
@ -0,0 +1,15 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct DiscoverDappsViewControllerViewModel { |
||||
|
||||
var dappCategories = Dapps.categorisedDapps |
||||
|
||||
var dapps = Dapps.masterList |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
} |
@ -0,0 +1,152 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct EditMyDappViewControllerViewModel { |
||||
let dapp: Bookmark |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Colors.appBackground |
||||
} |
||||
|
||||
var imageShadowColor: UIColor { |
||||
return .black |
||||
} |
||||
|
||||
var imageShadowOffset: CGSize { |
||||
return Metrics.DappsHome.Icon.shadowOffset |
||||
} |
||||
|
||||
var imageShadowOpacity: Float { |
||||
return Metrics.DappsHome.Icon.shadowOpacity |
||||
} |
||||
|
||||
var imageShadowRadius: CGFloat { |
||||
return Metrics.DappsHome.Icon.shadowRadius |
||||
} |
||||
|
||||
var imageBackgroundColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
|
||||
var imagePlaceholder: UIImage { |
||||
return R.image.launch_icon()! |
||||
} |
||||
|
||||
var imageUrl: URL? { |
||||
return Favicon.get(for: URL(string: dapp.url)) |
||||
} |
||||
|
||||
var screenTitle: String { |
||||
return R.string.localizable.dappBrowserMyDappsEdit() |
||||
} |
||||
|
||||
var screenFont: UIFont { |
||||
return Fonts.semibold(size: 20)! |
||||
} |
||||
|
||||
var titleColor: UIColor { |
||||
return .init(red: 71, green: 71, blue: 71) |
||||
} |
||||
|
||||
var titleFont: UIFont { |
||||
return Fonts.semibold(size: 16)! |
||||
} |
||||
|
||||
var titleText: String { |
||||
return R.string.localizable.dappBrowserMyDappsEditTitleLabel() |
||||
} |
||||
|
||||
var urlColor: UIColor { |
||||
return .init(red: 71, green: 71, blue: 71) |
||||
} |
||||
|
||||
var urlFont: UIFont { |
||||
return Fonts.semibold(size: 16)! |
||||
} |
||||
|
||||
var urlText: String { |
||||
return R.string.localizable.dappBrowserMyDappsEditUrlLabel() |
||||
} |
||||
|
||||
var titleTextFieldBorderStyle: UITextField.BorderStyle { |
||||
return .roundedRect |
||||
} |
||||
|
||||
var titleTextFieldBorderWidth: CGFloat { |
||||
return 0.5 |
||||
} |
||||
|
||||
var titleTextFieldBorderColor: UIColor { |
||||
return .init(red: 112, green: 112, blue: 112) |
||||
} |
||||
|
||||
var titleTextFieldCornerRadius: CGFloat { |
||||
return 7 |
||||
} |
||||
|
||||
var titleTextFieldFont: UIFont { |
||||
return Fonts.light(size: 16)! |
||||
} |
||||
|
||||
var titleTextFieldText: String { |
||||
return dapp.title |
||||
} |
||||
|
||||
var urlTextFieldBorderStyle: UITextField.BorderStyle { |
||||
return .roundedRect |
||||
} |
||||
|
||||
var urlTextFieldBorderWidth: CGFloat { |
||||
return 0.5 |
||||
} |
||||
|
||||
var urlTextFieldBorderColor: UIColor { |
||||
return .init(red: 112, green: 112, blue: 112) |
||||
} |
||||
|
||||
var urlTextFieldCornerRadius: CGFloat { |
||||
return 7 |
||||
} |
||||
|
||||
var urlTextFieldFont: UIFont { |
||||
return Fonts.light(size: 16)! |
||||
} |
||||
|
||||
var urlTextFieldText: String { |
||||
return dapp.url |
||||
} |
||||
|
||||
var saveButtonTitleColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
|
||||
var saveButtonBackgroundColor: UIColor { |
||||
return Colors.appHighlightGreen |
||||
} |
||||
|
||||
var saveButtonFont: UIFont { |
||||
return Fonts.regular(size: 20)! |
||||
} |
||||
|
||||
var saveButtonTitle: String { |
||||
return R.string.localizable.save() |
||||
} |
||||
|
||||
var saveButtonCornerRadius: CGFloat { |
||||
return 16 |
||||
} |
||||
|
||||
var cancelButtonTitleColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
|
||||
var cancelButtonFont: UIFont { |
||||
return Fonts.regular(size: 20)! |
||||
} |
||||
|
||||
var cancelButtonTitle: String { |
||||
return R.string.localizable.cancel() |
||||
} |
||||
} |
@ -1,27 +0,0 @@ |
||||
// Copyright DApps Platform Inc. All rights reserved. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct HistoryViewModel: URLViewModel { |
||||
|
||||
private let history: History |
||||
|
||||
init( |
||||
history: History |
||||
) { |
||||
self.history = history |
||||
} |
||||
|
||||
var urlText: String? { |
||||
return history.URL?.absoluteString |
||||
} |
||||
|
||||
var title: String { |
||||
return history.title |
||||
} |
||||
|
||||
var imageURL: URL? { |
||||
return Favicon.get(for: history.URL) |
||||
} |
||||
} |
@ -1,9 +0,0 @@ |
||||
// Copyright SIX DAY LLC. All rights reserved. |
||||
|
||||
import Foundation |
||||
|
||||
struct MarketplaceViewModel { |
||||
var title: String { |
||||
return R.string.localizable.aMarketplaceTabbarItemTitle() |
||||
} |
||||
} |
@ -0,0 +1,80 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
struct MyDappCellViewModel { |
||||
let dapp: Bookmark |
||||
|
||||
var backgroundColor: UIColor { |
||||
return Colors.appWhite |
||||
} |
||||
|
||||
var imageUrl: URL? { |
||||
return Favicon.get(for: URL(string: dapp.url)) |
||||
} |
||||
|
||||
var fallbackImage: UIImage? { |
||||
return R.image.launch_icon() |
||||
} |
||||
|
||||
var name: String { |
||||
return dapp.title |
||||
} |
||||
|
||||
var domainName: String { |
||||
return URL(string: dapp.url)?.host ?? "" |
||||
} |
||||
|
||||
var nameFont: UIFont { |
||||
return Fonts.semibold(size: 12)! |
||||
} |
||||
|
||||
var domainNameFont: UIFont { |
||||
return Fonts.bold(size: 10)! |
||||
} |
||||
|
||||
var nameColor: UIColor? { |
||||
return UIColor(red: 77, green: 77, blue: 77) |
||||
} |
||||
|
||||
var domainNameColor: UIColor? { |
||||
return Colors.appBackground |
||||
} |
||||
|
||||
var addRemoveButtonFont: UIFont { |
||||
return Fonts.semibold(size: 12)! |
||||
} |
||||
|
||||
var addRemoveButtonContentEdgeInsets: UIEdgeInsets { |
||||
return .init(top: 7, left: 14, bottom: 7, right: 14) |
||||
} |
||||
|
||||
var addRemoveButtonBorderColor: UIColor { |
||||
return Colors.appBackground |
||||
} |
||||
|
||||
var addRemoveButtonBorderWidth: CGFloat { |
||||
return 1 |
||||
} |
||||
|
||||
var addRemoveButtonBorderCornerRadius: CGFloat { |
||||
return 9 |
||||
} |
||||
|
||||
var imageViewShadowColor: UIColor { |
||||
return .black |
||||
} |
||||
|
||||
var imageViewShadowOffset: CGSize { |
||||
return Metrics.DappsHome.Icon.shadowOffset |
||||
} |
||||
|
||||
var imageViewShadowOpacity: Float { |
||||
return Metrics.DappsHome.Icon.shadowOpacity |
||||
} |
||||
|
||||
var imageViewShadowRadius: CGFloat { |
||||
return Metrics.DappsHome.Icon.shadowRadius |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
|
||||
struct MyDappsViewControllerViewModel { |
||||
var bookmarksStore: BookmarksStore |
||||
|
||||
var dappsCount: Int { |
||||
return bookmarksStore.bookmarks.count |
||||
} |
||||
|
||||
var hasContent: Bool { |
||||
return !bookmarksStore.bookmarks.isEmpty |
||||
} |
||||
|
||||
func dapp(atIndex index: Int) -> Bookmark { |
||||
return bookmarksStore.bookmarks[index] |
||||
} |
||||
} |
@ -0,0 +1,82 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
class BrowserHistoryCell: UITableViewCell { |
||||
static let identifier = "BrowserHistoryCell" |
||||
|
||||
private var viewModel: BrowserHistoryCellViewModel? |
||||
private let iconImageViewHolder = UIView() |
||||
|
||||
let iconImageView = UIImageView() |
||||
let titleLabel = UILabel() |
||||
let urlLabel = UILabel() |
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { |
||||
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) |
||||
|
||||
let labelsVerticalStackView = [ |
||||
titleLabel, |
||||
urlLabel |
||||
].asStackView(axis: .vertical) |
||||
|
||||
iconImageView.translatesAutoresizingMaskIntoConstraints = false |
||||
iconImageViewHolder.addSubview(iconImageView) |
||||
|
||||
let mainStackView = [.spacerWidth(29), iconImageViewHolder, .spacerWidth(26), labelsVerticalStackView, .spacerWidth(29)].asStackView(axis: .horizontal, alignment: .center) |
||||
mainStackView.translatesAutoresizingMaskIntoConstraints = false |
||||
contentView.addSubview(mainStackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
mainStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), |
||||
mainStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), |
||||
mainStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 7), |
||||
mainStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -7), |
||||
|
||||
iconImageView.widthAnchor.constraint(equalToConstant: 44), |
||||
iconImageView.widthAnchor.constraint(equalTo: iconImageView.heightAnchor), |
||||
|
||||
iconImageView.leadingAnchor.constraint(equalTo: iconImageViewHolder.leadingAnchor), |
||||
iconImageView.trailingAnchor.constraint(equalTo: iconImageViewHolder.trailingAnchor), |
||||
iconImageView.topAnchor.constraint(equalTo: iconImageViewHolder.topAnchor), |
||||
iconImageView.bottomAnchor.constraint(equalTo: iconImageViewHolder.bottomAnchor), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: BrowserHistoryCellViewModel) { |
||||
self.viewModel = viewModel |
||||
|
||||
backgroundColor = viewModel.backgroundColor |
||||
contentView.backgroundColor = viewModel.backgroundColor |
||||
|
||||
iconImageViewHolder.layer.shadowColor = viewModel.imageViewShadowColor.cgColor |
||||
iconImageViewHolder.layer.shadowOffset = viewModel.imageViewShadowOffset |
||||
iconImageViewHolder.layer.shadowOpacity = viewModel.imageViewShadowOpacity |
||||
iconImageViewHolder.layer.shadowRadius = viewModel.imageViewShadowRadius |
||||
|
||||
iconImageView.backgroundColor = viewModel.backgroundColor |
||||
iconImageView.contentMode = .scaleAspectFill |
||||
iconImageView.clipsToBounds = true |
||||
iconImageView.kf.setImage(with: viewModel.imageUrl, placeholder: viewModel.fallbackImage) |
||||
|
||||
titleLabel.font = viewModel.nameFont |
||||
titleLabel.textColor = viewModel.nameColor |
||||
titleLabel.text = viewModel.name |
||||
|
||||
urlLabel.font = viewModel.urlFont |
||||
urlLabel.textColor = viewModel.urlColor |
||||
urlLabel.text = viewModel.url |
||||
|
||||
//TODO ugly hack to get the image view's frame. Can't figure out a good point to retrieve the correct frame otherwise |
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { |
||||
self.iconImageView.layer.cornerRadius = self.iconImageView.frame.size.width / 2 |
||||
self.iconImageViewHolder.layer.cornerRadius = self.iconImageViewHolder.frame.size.width / 2 |
||||
self.iconImageViewHolder.layer.shadowPath = UIBezierPath(roundedRect: self.iconImageViewHolder.bounds, cornerRadius: self.iconImageViewHolder.layer.cornerRadius).cgPath |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,52 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
protocol BrowserHistoryViewControllerHeaderViewDelegate: class { |
||||
func didTapClearAll(inHeaderView headerView: BrowserHistoryViewControllerHeaderView) |
||||
} |
||||
|
||||
class BrowserHistoryViewControllerHeaderView: UIView { |
||||
private let header = DappsHomeHeaderView() |
||||
private let clearButton = UIButton(type: .system) |
||||
|
||||
weak var delegate: BrowserHistoryViewControllerHeaderViewDelegate? |
||||
|
||||
init() { |
||||
super.init(frame: .zero) |
||||
|
||||
header.translatesAutoresizingMaskIntoConstraints = false |
||||
addSubview(header) |
||||
|
||||
clearButton.addTarget(self, action: #selector(clearHistory), for: .touchUpInside) |
||||
clearButton.translatesAutoresizingMaskIntoConstraints = false |
||||
addSubview(clearButton) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
header.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||
header.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||
header.topAnchor.constraint(equalTo: topAnchor, constant: 50), |
||||
header.bottomAnchor.constraint(equalTo: clearButton.topAnchor, constant: -30), |
||||
|
||||
clearButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), |
||||
clearButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: DappsHomeHeaderViewViewModel) { |
||||
backgroundColor = viewModel.backgroundColor |
||||
header.configure(viewModel: viewModel) |
||||
|
||||
clearButton.setTitle(R.string.localizable.clearButtonTitle().localizedUppercase, for: .normal) |
||||
clearButton.titleLabel?.font = Fonts.bold(size: 12) |
||||
} |
||||
|
||||
@objc private func clearHistory() { |
||||
delegate?.didTapClearAll(inHeaderView: self) |
||||
} |
||||
} |
@ -1,120 +0,0 @@ |
||||
// Copyright DApps Platform Inc. All rights reserved. |
||||
|
||||
import UIKit |
||||
|
||||
protocol BrowserNavigationBarDelegate: class { |
||||
func did(action: BrowserNavigation) |
||||
} |
||||
|
||||
final class BrowserNavigationBar: UINavigationBar { |
||||
private let moreButton = UIButton() |
||||
private let homeButton = UIButton() |
||||
|
||||
private struct Layout { |
||||
static let width: CGFloat = 34 |
||||
static let moreButtonWidth: CGFloat = 24 |
||||
} |
||||
|
||||
let textField = UITextField() |
||||
let closeButton = UIButton() |
||||
let backButton = UIButton() |
||||
weak var browserDelegate: BrowserNavigationBarDelegate? |
||||
|
||||
override init(frame: CGRect) { |
||||
super.init(frame: frame) |
||||
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false |
||||
textField.backgroundColor = .white |
||||
textField.layer.cornerRadius = 5 |
||||
textField.layer.borderWidth = 0.5 |
||||
textField.layer.borderColor = Colors.lightGray.cgColor |
||||
textField.autocapitalizationType = .none |
||||
textField.autoresizingMask = .flexibleWidth |
||||
textField.delegate = self |
||||
textField.autocorrectionType = .no |
||||
textField.returnKeyType = .go |
||||
textField.clearButtonMode = .whileEditing |
||||
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 6, height: 30)) |
||||
textField.leftViewMode = .always |
||||
textField.placeholder = R.string.localizable.browserUrlTextfieldPlaceholder() |
||||
textField.keyboardType = .webSearch |
||||
|
||||
moreButton.translatesAutoresizingMaskIntoConstraints = false |
||||
moreButton.setImage(R.image.toolbarMenu(), for: .normal) |
||||
moreButton.addTarget(self, action: #selector(moreAction(_:)), for: .touchUpInside) |
||||
|
||||
closeButton.translatesAutoresizingMaskIntoConstraints = false |
||||
closeButton.isHidden = true |
||||
closeButton.setTitle(R.string.localizable.done(), for: .normal) |
||||
closeButton.addTarget(self, action: #selector(closeAction(_:)), for: .touchUpInside) |
||||
closeButton.setContentCompressionResistancePriority(.required, for: .horizontal) |
||||
closeButton.setContentHuggingPriority(.required, for: .horizontal) |
||||
|
||||
homeButton.translatesAutoresizingMaskIntoConstraints = false |
||||
homeButton.setImage(R.image.browserHome()?.withRenderingMode(.alwaysTemplate), for: .normal) |
||||
homeButton.addTarget(self, action: #selector(homeAction(_:)), for: .touchUpInside) |
||||
|
||||
backButton.translatesAutoresizingMaskIntoConstraints = false |
||||
backButton.setImage(R.image.toolbarBack(), for: .normal) |
||||
backButton.addTarget(self, action: #selector(goBackAction), for: .touchUpInside) |
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [ |
||||
closeButton, |
||||
homeButton, |
||||
.spacerWidth(), |
||||
backButton, |
||||
textField, |
||||
.spacerWidth(), |
||||
moreButton, |
||||
]) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
stackView.axis = .horizontal |
||||
stackView.distribution = .fill |
||||
stackView.spacing = 4 |
||||
|
||||
addSubview(stackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 4), |
||||
stackView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor, constant: 10), |
||||
stackView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor, constant: -10), |
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -6), |
||||
|
||||
homeButton.widthAnchor.constraint(equalToConstant: Layout.width), |
||||
backButton.widthAnchor.constraint(equalToConstant: Layout.width), |
||||
moreButton.widthAnchor.constraint(equalToConstant: Layout.moreButtonWidth), |
||||
]) |
||||
} |
||||
|
||||
@objc private func goBackAction() { |
||||
browserDelegate?.did(action: .goBack) |
||||
} |
||||
|
||||
@objc private func moreAction(_ sender: UIView) { |
||||
browserDelegate?.did(action: .more(sender: sender)) |
||||
} |
||||
|
||||
@objc private func homeAction(_ sender: UIView) { |
||||
browserDelegate?.did(action: .home) |
||||
} |
||||
|
||||
@objc private func closeAction(_ sender: UIView) { |
||||
browserDelegate?.did(action: .close) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
} |
||||
|
||||
extension BrowserNavigationBar: UITextFieldDelegate { |
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool { |
||||
browserDelegate?.did(action: .enter(textField.text ?? "")) |
||||
textField.resignFirstResponder() |
||||
return true |
||||
} |
||||
|
||||
func textFieldDidBeginEditing(_ textField: UITextField) { |
||||
browserDelegate?.did(action: .beginEditing) |
||||
} |
||||
} |
@ -0,0 +1,298 @@ |
||||
// Copyright DApps Platform Inc. All rights reserved. |
||||
|
||||
import UIKit |
||||
|
||||
protocol DappBrowserNavigationBarDelegate: class { |
||||
func didTyped(text: String, inNavigationBar navigationBar: DappBrowserNavigationBar) |
||||
func didEnter(text: String, inNavigationBar navigationBar: DappBrowserNavigationBar) |
||||
func didTapHome(inNavigationBar navigationBar: DappBrowserNavigationBar) |
||||
func didTapBack(inNavigationBar navigationBar: DappBrowserNavigationBar) |
||||
func didTapForward(inNavigationBar navigationBar: DappBrowserNavigationBar) |
||||
func didTapMore(sender: UIView, inNavigationBar navigationBar: DappBrowserNavigationBar) |
||||
func didTapClose(inNavigationBar navigationBar: DappBrowserNavigationBar) |
||||
} |
||||
|
||||
fileprivate enum State { |
||||
case editingURLTextField |
||||
case notEditingURLTextField |
||||
case browserOnly |
||||
} |
||||
|
||||
fileprivate struct Layout { |
||||
static let width: CGFloat = 34 |
||||
static let moreButtonWidth: CGFloat = 24 |
||||
} |
||||
|
||||
final class DappBrowserNavigationBar: UINavigationBar { |
||||
private let moreButton = UIButton() |
||||
private let homeButton = UIButton() |
||||
private let cancelEditingButton = UIButton() |
||||
private let closeButton = UIButton() |
||||
|
||||
private let textField = UITextField() |
||||
private let domainNameLabel = UILabel() |
||||
private let backButton = UIButton() |
||||
private let forwardButton = UIButton() |
||||
private var viewsToShowWhenNotEditing = [UIView]() |
||||
private var viewsToShowWhenEditing = [UIView]() |
||||
private var viewsToShowWhenBrowserOnly = [UIView]() |
||||
private var state = State.notEditingURLTextField { |
||||
didSet { |
||||
var show: [UIView] |
||||
var hide: [UIView] |
||||
switch state { |
||||
case .editingURLTextField: |
||||
hide = viewsToShowWhenNotEditing + viewsToShowWhenBrowserOnly - viewsToShowWhenEditing |
||||
show = viewsToShowWhenEditing |
||||
case .notEditingURLTextField: |
||||
hide = viewsToShowWhenEditing + viewsToShowWhenBrowserOnly - viewsToShowWhenNotEditing |
||||
show = viewsToShowWhenNotEditing |
||||
case .browserOnly: |
||||
hide = viewsToShowWhenEditing + viewsToShowWhenNotEditing - viewsToShowWhenBrowserOnly |
||||
show = viewsToShowWhenBrowserOnly |
||||
} |
||||
hide.hideAll() |
||||
show.showAll() |
||||
} |
||||
} |
||||
var isBrowserOnly: Bool { |
||||
return state == .browserOnly |
||||
} |
||||
|
||||
weak var navigationBarDelegate: DappBrowserNavigationBarDelegate? |
||||
|
||||
override init(frame: CGRect) { |
||||
super.init(frame: frame) |
||||
|
||||
textField.backgroundColor = .white |
||||
textField.layer.cornerRadius = 5 |
||||
textField.layer.borderWidth = 0.5 |
||||
textField.layer.borderColor = Colors.lightGray.cgColor |
||||
textField.autocapitalizationType = .none |
||||
textField.autoresizingMask = .flexibleWidth |
||||
textField.delegate = self |
||||
textField.autocorrectionType = .no |
||||
textField.returnKeyType = .go |
||||
textField.clearButtonMode = .whileEditing |
||||
textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 6, height: 30)) |
||||
textField.leftViewMode = .always |
||||
textField.placeholder = R.string.localizable.browserUrlTextfieldPlaceholder() |
||||
textField.keyboardType = .webSearch |
||||
|
||||
domainNameLabel.isHidden = true |
||||
|
||||
moreButton.setImage(R.image.toolbarMenu(), for: .normal) |
||||
moreButton.addTarget(self, action: #selector(moreAction(_:)), for: .touchUpInside) |
||||
|
||||
closeButton.isHidden = true |
||||
closeButton.setTitle(R.string.localizable.done(), for: .normal) |
||||
closeButton.addTarget(self, action: #selector(closeAction(_:)), for: .touchUpInside) |
||||
closeButton.setContentCompressionResistancePriority(.required, for: .horizontal) |
||||
closeButton.setContentHuggingPriority(.required, for: .horizontal) |
||||
|
||||
homeButton.setImage(R.image.browserHome()?.withRenderingMode(.alwaysTemplate), for: .normal) |
||||
homeButton.addTarget(self, action: #selector(homeAction(_:)), for: .touchUpInside) |
||||
|
||||
backButton.setImage(R.image.toolbarBack(), for: .normal) |
||||
backButton.addTarget(self, action: #selector(goBackAction), for: .touchUpInside) |
||||
|
||||
forwardButton.setImage(R.image.toolbarForward(), for: .normal) |
||||
forwardButton.addTarget(self, action: #selector(goForwardAction), for: .touchUpInside) |
||||
|
||||
//compression and hugging priority required to make cancel button appear reliably yet not be too wide |
||||
cancelEditingButton.setContentCompressionResistancePriority(.required, for: .horizontal) |
||||
cancelEditingButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) |
||||
cancelEditingButton.addTarget(self, action: #selector(cancelEditing), for: .touchUpInside) |
||||
|
||||
let spacer0 = UIView.spacerWidth() |
||||
let spacer1 = UIView.spacerWidth() |
||||
let spacer2 = UIView.spacerWidth() |
||||
viewsToShowWhenNotEditing.append(contentsOf: [spacer0, spacer1, backButton, forwardButton, textField, homeButton, spacer2, moreButton]) |
||||
viewsToShowWhenEditing.append(contentsOf: [textField, cancelEditingButton]) |
||||
viewsToShowWhenBrowserOnly.append(contentsOf: [spacer0, backButton, forwardButton, domainNameLabel, spacer1, closeButton, spacer2, moreButton]) |
||||
|
||||
cancelEditingButton.isHidden = true |
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [ |
||||
spacer0, |
||||
backButton, |
||||
forwardButton, |
||||
textField, |
||||
domainNameLabel, |
||||
spacer1, |
||||
homeButton, |
||||
closeButton, |
||||
spacer2, |
||||
moreButton, |
||||
cancelEditingButton, |
||||
]) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
stackView.axis = .horizontal |
||||
stackView.distribution = .fill |
||||
stackView.spacing = 4 |
||||
|
||||
addSubview(stackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 4), |
||||
stackView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor, constant: 10), |
||||
stackView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor, constant: -10), |
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -6), |
||||
|
||||
homeButton.widthAnchor.constraint(equalToConstant: Layout.width), |
||||
backButton.widthAnchor.constraint(equalToConstant: Layout.width), |
||||
forwardButton.widthAnchor.constraint(equalToConstant: Layout.width), |
||||
moreButton.widthAnchor.constraint(equalToConstant: Layout.moreButtonWidth), |
||||
]) |
||||
|
||||
configure() |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
private func configure() { |
||||
let color = Colors.appWhite |
||||
backButton.imageView?.tintColor = color |
||||
forwardButton.imageView?.tintColor = color |
||||
homeButton.imageView?.tintColor = color |
||||
moreButton.imageView?.tintColor = color |
||||
|
||||
domainNameLabel.textColor = color |
||||
domainNameLabel.textAlignment = .center |
||||
|
||||
cancelEditingButton.setTitle(R.string.localizable.cancel(), for: .normal) |
||||
} |
||||
|
||||
@objc private func goBackAction() { |
||||
cancelEditing() |
||||
navigationBarDelegate?.didTapBack(inNavigationBar: self) |
||||
} |
||||
|
||||
@objc private func goForwardAction() { |
||||
cancelEditing() |
||||
navigationBarDelegate?.didTapForward(inNavigationBar: self) |
||||
} |
||||
|
||||
@objc private func moreAction(_ sender: UIView) { |
||||
cancelEditing() |
||||
navigationBarDelegate?.didTapMore(sender: sender, inNavigationBar: self) |
||||
} |
||||
|
||||
@objc private func homeAction(_ sender: UIView) { |
||||
cancelEditing() |
||||
navigationBarDelegate?.didTapHome(inNavigationBar: self) |
||||
} |
||||
|
||||
@objc private func closeAction(_ sender: UIView) { |
||||
cancelEditing() |
||||
navigationBarDelegate?.didTapClose(inNavigationBar: self) |
||||
} |
||||
|
||||
//TODO this might get triggered immediately if we use a physical keyboard. Verify |
||||
@objc func cancelEditing() { |
||||
dismissKeyboard() |
||||
switch state { |
||||
case .editingURLTextField: |
||||
UIView.animate(withDuration: 0.3) { |
||||
self.state = .notEditingURLTextField |
||||
} |
||||
case .notEditingURLTextField, .browserOnly: |
||||
//We especially don't want to switch (and animate) to .notEditingURLTextField when we are closing .browserOnly mode |
||||
break |
||||
} |
||||
} |
||||
|
||||
func display(url: URL) { |
||||
textField.text = url.absoluteString |
||||
domainNameLabel.text = URL(string: url.absoluteString)?.host ?? "" |
||||
} |
||||
|
||||
func display(string: String) { |
||||
textField.text = string |
||||
} |
||||
|
||||
func clearDisplay() { |
||||
display(string: "") |
||||
} |
||||
|
||||
private func dismissKeyboard() { |
||||
endEditing(true) |
||||
} |
||||
|
||||
func makeBrowserOnly() { |
||||
state = .browserOnly |
||||
} |
||||
|
||||
func disableButtons() { |
||||
backButton.isEnabled = false |
||||
forwardButton.isEnabled = false |
||||
homeButton.isEnabled = false |
||||
moreButton.isEnabled = false |
||||
textField.isEnabled = false |
||||
cancelEditingButton.isEnabled = false |
||||
closeButton.isEnabled = false |
||||
} |
||||
|
||||
func enableButtons() { |
||||
backButton.isEnabled = true |
||||
forwardButton.isEnabled = true |
||||
homeButton.isEnabled = true |
||||
moreButton.isEnabled = true |
||||
textField.isEnabled = true |
||||
cancelEditingButton.isEnabled = true |
||||
closeButton.isEnabled = true |
||||
} |
||||
} |
||||
|
||||
extension DappBrowserNavigationBar: UITextFieldDelegate { |
||||
private func queue(typedText text: String) { |
||||
navigationBarDelegate?.didTyped(text: text, inNavigationBar: self) |
||||
} |
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool { |
||||
navigationBarDelegate?.didEnter(text: textField.text ?? "", inNavigationBar: self) |
||||
textField.resignFirstResponder() |
||||
return true |
||||
} |
||||
|
||||
func textField(_ textField: UITextField, |
||||
shouldChangeCharactersIn range: NSRange, |
||||
replacementString string: String |
||||
) -> Bool { |
||||
if let text = textField.text, let range = Range(range, in: text) { |
||||
queue(typedText: textField.text?.replacingCharacters(in: range, with: string) ?? "") |
||||
} else { |
||||
queue(typedText: "") |
||||
} |
||||
return true |
||||
} |
||||
|
||||
func textFieldShouldClear(_ textField: UITextField) -> Bool { |
||||
queue(typedText: "") |
||||
return true |
||||
} |
||||
|
||||
public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { |
||||
UIView.animate(withDuration: 0.3) { |
||||
self.state = .editingURLTextField |
||||
} |
||||
return true |
||||
} |
||||
} |
||||
|
||||
//TODO move when we use this more |
||||
fileprivate extension Collection where Element == UIView { |
||||
func hideAll() { |
||||
for each in self { |
||||
each.isHidden = true |
||||
} |
||||
} |
||||
|
||||
func showAll() { |
||||
for each in self { |
||||
each.isHidden = false |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,43 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
class DappButton: UIControl { |
||||
|
||||
private let imageView = UIImageView() |
||||
private let label = UILabel() |
||||
|
||||
override init(frame: CGRect) { |
||||
super.init(frame: frame) |
||||
|
||||
let stackView = [imageView, label].asStackView( |
||||
axis: .vertical, |
||||
contentHuggingPriority: .required, |
||||
alignment: .center |
||||
) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
stackView.isUserInteractionEnabled = false |
||||
addSubview(stackView) |
||||
NSLayoutConstraint.activate([ |
||||
stackView.topAnchor.constraint(equalTo: topAnchor), |
||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor), |
||||
imageView.widthAnchor.constraint(equalToConstant: 50), |
||||
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: DappButtonViewModel) { |
||||
imageView.image = viewModel.image |
||||
|
||||
label.font = viewModel.font |
||||
label.textColor = viewModel.textColor |
||||
label.text = viewModel.title |
||||
} |
||||
} |
@ -0,0 +1,158 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
protocol DappViewCellDelegate: class { |
||||
func didTapDelete(dapp: Bookmark, inCell cell: DappViewCell) |
||||
func didLongPressed(dapp: Bookmark, onCell cell: DappViewCell) |
||||
} |
||||
|
||||
//hboon because of how we implemented shadows parallex doesn't work anymore. Fix it again by adding another wrapper around imageHolder? Maybe shadow should just be implemented with a sublayer |
||||
class DappViewCell: UICollectionViewCell { |
||||
static let identifier = "DappViewCell" |
||||
|
||||
private let marginAroundImage = CGFloat(7) |
||||
private let jiggleAnimationKey = "jiggle" |
||||
private var viewModel: DappViewCellViewModel? |
||||
private var currentDisplayedImageUrl: URL? |
||||
private let background = UIView() |
||||
private let imageView = UIImageView() |
||||
//Holder to show the shadow around the image because the UIImageView is clipsToBounds=true |
||||
private let imageHolder = UIView() |
||||
private let titleLabel = UILabel() |
||||
private let domainLabel = UILabel() |
||||
private let deleteButton = UIButton(type: .system) |
||||
var isEditing: Bool = false { |
||||
didSet { |
||||
if isEditing { |
||||
let randomNumber = CGFloat(arc4random_uniform(500)) / 500 + 0.5 |
||||
let angle = CGFloat(0.06 * randomNumber) |
||||
let left = CATransform3DMakeRotation(angle, 0, 0, 1) |
||||
let right = CATransform3DMakeRotation(-angle, 0, 0, 1) |
||||
let animation = CAKeyframeAnimation(keyPath: "transform") |
||||
animation.values = [left, right] |
||||
animation.autoreverses = true |
||||
animation.duration = 0.125 |
||||
animation.repeatCount = Float(Int.max) |
||||
contentView.layer.add(animation, forKey: jiggleAnimationKey) |
||||
} else { |
||||
contentView.layer.removeAnimation(forKey: jiggleAnimationKey) |
||||
} |
||||
deleteButton.isHidden = !isEditing |
||||
} |
||||
} |
||||
weak var delegate: DappViewCellDelegate? |
||||
|
||||
override init(frame: CGRect) { |
||||
super.init(frame: frame) |
||||
|
||||
background.translatesAutoresizingMaskIntoConstraints = false |
||||
contentView.addSubview(background) |
||||
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false |
||||
imageHolder.addSubview(imageView) |
||||
|
||||
let stackView = [ |
||||
.spacer(height: marginAroundImage), |
||||
imageHolder, |
||||
.spacer(height: 9), |
||||
titleLabel, |
||||
domainLabel, |
||||
].asStackView(axis: .vertical, spacing: 0, alignment: .center) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
background.addSubview(stackView) |
||||
|
||||
deleteButton.addTarget(self, action: #selector(deleteDapp), for: .touchUpInside) |
||||
deleteButton.isHidden = true |
||||
deleteButton.translatesAutoresizingMaskIntoConstraints = false |
||||
background.addSubview(deleteButton) |
||||
|
||||
let xMargin = CGFloat(0) |
||||
let yMargin = CGFloat(0) |
||||
NSLayoutConstraint.activate([ |
||||
background.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: xMargin), |
||||
background.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -xMargin), |
||||
background.topAnchor.constraint(equalTo: contentView.topAnchor, constant: yMargin), |
||||
background.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -yMargin), |
||||
|
||||
stackView.leadingAnchor.constraint(equalTo: background.leadingAnchor), |
||||
stackView.trailingAnchor.constraint(equalTo: background.trailingAnchor), |
||||
stackView.topAnchor.constraint(equalTo: background.topAnchor), |
||||
stackView.bottomAnchor.constraint(equalTo: background.bottomAnchor), |
||||
|
||||
imageHolder.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: marginAroundImage), |
||||
imageHolder.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -marginAroundImage), |
||||
imageHolder.widthAnchor.constraint(equalTo: imageHolder.heightAnchor), |
||||
|
||||
imageView.leadingAnchor.constraint(equalTo: imageHolder.leadingAnchor), |
||||
imageView.trailingAnchor.constraint(equalTo: imageHolder.trailingAnchor), |
||||
imageView.topAnchor.constraint(equalTo: imageHolder.topAnchor), |
||||
imageView.bottomAnchor.constraint(equalTo: imageHolder.bottomAnchor), |
||||
|
||||
deleteButton.rightAnchor.constraint(equalTo: contentView.rightAnchor), |
||||
//Some allowance so the delete button is not clipped while jiggling |
||||
deleteButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5), |
||||
deleteButton.widthAnchor.constraint(equalToConstant: 22), |
||||
deleteButton.widthAnchor.constraint(equalTo: deleteButton.heightAnchor), |
||||
]) |
||||
|
||||
addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(longPressedDappCell))) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
override func layoutSubviews() { |
||||
super.layoutSubviews() |
||||
|
||||
imageHolder.layer.cornerRadius = imageHolder.frame.size.width / 2 |
||||
imageView.layer.cornerRadius = imageView.frame.size.width / 2 |
||||
|
||||
imageHolder.layer.shadowPath = UIBezierPath(roundedRect: imageHolder.bounds, cornerRadius: imageHolder.layer.cornerRadius).cgPath |
||||
} |
||||
|
||||
func configure(viewModel: DappViewCellViewModel) { |
||||
self.viewModel = viewModel |
||||
|
||||
contentView.backgroundColor = viewModel.backgroundColor |
||||
|
||||
background.backgroundColor = viewModel.backgroundColor |
||||
background.clipsToBounds = true |
||||
|
||||
imageHolder.layer.shadowColor = viewModel.imageViewShadowColor.cgColor |
||||
imageHolder.layer.shadowOffset = viewModel.imageViewShadowOffset |
||||
imageHolder.layer.shadowOpacity = viewModel.imageViewShadowOpacity |
||||
imageHolder.layer.shadowRadius = viewModel.imageViewShadowRadius |
||||
|
||||
imageView.backgroundColor = viewModel.backgroundColor |
||||
imageView.contentMode = .scaleAspectFill |
||||
imageView.clipsToBounds = true |
||||
imageView.kf.setImage(with: viewModel.imageUrl, placeholder: viewModel.fallbackImage) |
||||
|
||||
deleteButton.tintColor = Colors.appRed |
||||
deleteButton.imageView?.tintColor = Colors.appRed |
||||
deleteButton.setImage(R.image.onboarding_failed(), for: .normal) |
||||
|
||||
titleLabel.textAlignment = .center |
||||
titleLabel.textColor = viewModel.titleColor |
||||
titleLabel.font = viewModel.titleFont |
||||
titleLabel.text = viewModel.title |
||||
|
||||
domainLabel.textAlignment = .center |
||||
domainLabel.textColor = viewModel.domainNameColor |
||||
domainLabel.font = viewModel.domainNameFont |
||||
domainLabel.text = viewModel.domainName |
||||
} |
||||
|
||||
@objc func deleteDapp() { |
||||
guard let dapp = viewModel?.dapp else { return } |
||||
delegate?.didTapDelete(dapp: dapp, inCell: self) |
||||
} |
||||
|
||||
@objc private func longPressedDappCell() { |
||||
guard let dapp = viewModel?.dapp else { return } |
||||
delegate?.didLongPressed(dapp: dapp, onCell: self) |
||||
} |
||||
} |
@ -0,0 +1,44 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
class DappsAutoCompletionCell: UITableViewCell { |
||||
static let identifier = "DappsAutoCompletionCell" |
||||
let titleLabel = UILabel() |
||||
let descriptionLabel = UILabel() |
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { |
||||
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) |
||||
|
||||
let stackView = [ |
||||
titleLabel, |
||||
descriptionLabel |
||||
].asStackView(axis: .vertical) |
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
contentView.addSubview(stackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 28), |
||||
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -28), |
||||
stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), |
||||
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: DappsAutoCompletionCellViewModel) { |
||||
backgroundColor = viewModel.backgroundColor |
||||
contentView.backgroundColor = viewModel.backgroundColor |
||||
|
||||
titleLabel.font = viewModel.nameFont |
||||
titleLabel.attributedText = viewModel.name |
||||
|
||||
descriptionLabel.font = viewModel.descriptionFont |
||||
descriptionLabel.textColor = viewModel.descriptionColor |
||||
descriptionLabel.text = viewModel.description |
||||
} |
||||
} |
@ -0,0 +1,43 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
class DappsHomeEmptyView: UIView { |
||||
private let header = DappsHomeHeaderView() |
||||
private let label = UILabel() |
||||
|
||||
init() { |
||||
super.init(frame: .zero) |
||||
|
||||
header.translatesAutoresizingMaskIntoConstraints = false |
||||
addSubview(header) |
||||
|
||||
label.translatesAutoresizingMaskIntoConstraints = false |
||||
addSubview(label) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
header.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||
header.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||
header.topAnchor.constraint(equalTo: topAnchor, constant: 50), |
||||
|
||||
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 50), |
||||
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -50), |
||||
label.centerYAnchor.constraint(equalTo: centerYAnchor), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: DappsHomeEmptyViewViewModel) { |
||||
backgroundColor = viewModel.headerViewViewModel.backgroundColor |
||||
header.configure(viewModel: viewModel.headerViewViewModel) |
||||
|
||||
label.font = Fonts.light(size: 18) |
||||
label.textAlignment = .center |
||||
label.numberOfLines = 0 |
||||
label.text = viewModel.title |
||||
} |
||||
} |
@ -0,0 +1,47 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
class DappsHomeHeaderView: UIView { |
||||
private let stackView = [].asStackView(axis: .vertical, contentHuggingPriority: .required, alignment: .center) |
||||
private let buttonsStackView = [].asStackView(distribution: .equalSpacing, contentHuggingPriority: .required) |
||||
private let logoImage = UIImageView() |
||||
private let titleLabel = UILabel() |
||||
|
||||
override init(frame: CGRect) { |
||||
super.init(frame: frame) |
||||
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
stackView.addArrangedSubviews([ |
||||
logoImage, |
||||
.spacer(height: 20), |
||||
titleLabel, |
||||
]) |
||||
addSubview(stackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
stackView.topAnchor.constraint(equalTo: topAnchor), |
||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor), |
||||
|
||||
logoImage.widthAnchor.constraint(equalToConstant: 80), |
||||
logoImage.widthAnchor.constraint(equalTo: logoImage.heightAnchor), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: DappsHomeHeaderViewViewModel) { |
||||
backgroundColor = viewModel.backgroundColor |
||||
|
||||
logoImage.contentMode = .scaleAspectFit |
||||
logoImage.image = viewModel.logo |
||||
|
||||
titleLabel.font = viewModel.titleFont |
||||
titleLabel.text = viewModel.title |
||||
} |
||||
} |
@ -0,0 +1,59 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
class DappsHomeViewControllerHeaderView: UICollectionReusableView { |
||||
private let stackView = [].asStackView(axis: .vertical, contentHuggingPriority: .required, alignment: .center) |
||||
private let headerView = DappsHomeHeaderView() |
||||
|
||||
let myDappsButton = DappButton() |
||||
let discoverDappsButton = DappButton() |
||||
let historyButton = DappButton() |
||||
|
||||
override init(frame: CGRect) { |
||||
super.init(frame: frame) |
||||
|
||||
let buttonsStackView = [ |
||||
myDappsButton, |
||||
.spacerWidth(40), |
||||
discoverDappsButton, |
||||
.spacerWidth(40), |
||||
historyButton |
||||
].asStackView(distribution: .equalSpacing, contentHuggingPriority: .required) |
||||
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false |
||||
stackView.addArrangedSubviews([ |
||||
headerView, |
||||
.spacer(height: 30), |
||||
buttonsStackView, |
||||
]) |
||||
addSubview(stackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 50), |
||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -50), |
||||
|
||||
myDappsButton.widthAnchor.constraint(equalTo: discoverDappsButton.widthAnchor), |
||||
myDappsButton.widthAnchor.constraint(equalTo: historyButton.widthAnchor), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: DappsHomeViewControllerHeaderViewViewModel = .init()) { |
||||
backgroundColor = viewModel.backgroundColor |
||||
|
||||
headerView.configure(viewModel: .init(title: viewModel.title)) |
||||
|
||||
myDappsButton.configure(viewModel: .init(image: viewModel.myDappsButtonImage, title: viewModel.myDappsButtonTitle)) |
||||
|
||||
discoverDappsButton.configure(viewModel: .init(image: viewModel.discoverButtonImage, title: viewModel.discoverButtonTitle)) |
||||
|
||||
historyButton.configure(viewModel: .init(image: viewModel.historyButtonImage, title: viewModel.historyButtonTitle)) |
||||
} |
||||
} |
@ -0,0 +1,121 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
protocol DiscoverDappCellDelegate: class { |
||||
func onAdd(dapp: Dapp, inCell cell: DiscoverDappCell) |
||||
func onRemove(dapp: Dapp, inCell cell: DiscoverDappCell) |
||||
} |
||||
|
||||
class DiscoverDappCell: UITableViewCell { |
||||
static let identifier = "DiscoverDappCell" |
||||
|
||||
private let addButton = UIButton(type: .system) |
||||
private let removeButton = UIButton(type: .system) |
||||
private var viewModel: DiscoverDappCellViewModel? |
||||
private let iconImageViewHolder = UIView() |
||||
|
||||
let iconImageView = UIImageView() |
||||
let titleLabel = UILabel() |
||||
let descriptionLabel = UILabel() |
||||
weak var delegate: DiscoverDappCellDelegate? |
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { |
||||
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) |
||||
|
||||
let labelsVerticalStackView = [ |
||||
titleLabel, |
||||
descriptionLabel |
||||
].asStackView(axis: .vertical) |
||||
|
||||
addButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) |
||||
removeButton.setContentHuggingPriority(.defaultHigh, for: .horizontal) |
||||
|
||||
iconImageView.translatesAutoresizingMaskIntoConstraints = false |
||||
iconImageViewHolder.addSubview(iconImageView) |
||||
|
||||
let mainStackView = [.spacerWidth(29), iconImageViewHolder, .spacerWidth(26), labelsVerticalStackView, .spacerWidth(26), addButton, removeButton, .spacerWidth(29)].asStackView(axis: .horizontal, alignment: .center) |
||||
mainStackView.translatesAutoresizingMaskIntoConstraints = false |
||||
contentView.addSubview(mainStackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
mainStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), |
||||
mainStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), |
||||
mainStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 7), |
||||
mainStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -7), |
||||
|
||||
iconImageView.widthAnchor.constraint(equalToConstant: 44), |
||||
iconImageView.widthAnchor.constraint(equalTo: iconImageView.heightAnchor), |
||||
|
||||
iconImageView.leadingAnchor.constraint(equalTo: iconImageViewHolder.leadingAnchor), |
||||
iconImageView.trailingAnchor.constraint(equalTo: iconImageViewHolder.trailingAnchor), |
||||
iconImageView.topAnchor.constraint(equalTo: iconImageViewHolder.topAnchor), |
||||
iconImageView.bottomAnchor.constraint(equalTo: iconImageViewHolder.bottomAnchor), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: DiscoverDappCellViewModel) { |
||||
self.viewModel = viewModel |
||||
|
||||
backgroundColor = viewModel.backgroundColor |
||||
contentView.backgroundColor = viewModel.backgroundColor |
||||
|
||||
addButton.addTarget(self, action: #selector(onTappedAdd), for: .touchUpInside) |
||||
addButton.setTitle(R.string.localizable.addButtonTitle().localizedUppercase, for: .normal) |
||||
addButton.isHidden = viewModel.isAddButtonHidden |
||||
addButton.titleLabel?.font = viewModel.addRemoveButtonFont |
||||
addButton.contentEdgeInsets = viewModel.addRemoveButtonContentEdgeInsets |
||||
addButton.borderColor = viewModel.addRemoveButtonBorderColor |
||||
addButton.borderWidth = viewModel.addRemoveButtonBorderWidth |
||||
addButton.cornerRadius = viewModel.addRemoveButtonBorderCornerRadius |
||||
|
||||
removeButton.addTarget(self, action: #selector(onTappedRemove), for: .touchUpInside) |
||||
removeButton.setTitle(R.string.localizable.removeButtonTitle().localizedUppercase, for: .normal) |
||||
removeButton.isHidden = viewModel.isRemoveButtonHidden |
||||
removeButton.titleLabel?.font = viewModel.addRemoveButtonFont |
||||
removeButton.contentEdgeInsets = viewModel.addRemoveButtonContentEdgeInsets |
||||
removeButton.borderColor = viewModel.addRemoveButtonBorderColor |
||||
removeButton.borderWidth = viewModel.addRemoveButtonBorderWidth |
||||
removeButton.cornerRadius = viewModel.addRemoveButtonBorderCornerRadius |
||||
|
||||
iconImageViewHolder.layer.shadowColor = viewModel.imageViewShadowColor.cgColor |
||||
iconImageViewHolder.layer.shadowOffset = viewModel.imageViewShadowOffset |
||||
iconImageViewHolder.layer.shadowOpacity = viewModel.imageViewShadowOpacity |
||||
iconImageViewHolder.layer.shadowRadius = viewModel.imageViewShadowRadius |
||||
|
||||
iconImageView.backgroundColor = viewModel.backgroundColor |
||||
iconImageView.contentMode = .scaleAspectFill |
||||
iconImageView.clipsToBounds = true |
||||
iconImageView.kf.setImage(with: viewModel.imageUrl, placeholder: viewModel.fallbackImage) |
||||
|
||||
titleLabel.font = viewModel.nameFont |
||||
titleLabel.textColor = viewModel.nameColor |
||||
titleLabel.text = viewModel.name |
||||
|
||||
descriptionLabel.font = viewModel.descriptionFont |
||||
descriptionLabel.textColor = viewModel.descriptionColor |
||||
descriptionLabel.text = viewModel.description |
||||
|
||||
//TODO ugly hack to get the image view's frame. Can't figure out a good point to retrieve the correct frame otherwise |
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { |
||||
self.iconImageView.layer.cornerRadius = self.iconImageView.frame.size.width / 2 |
||||
self.iconImageViewHolder.layer.cornerRadius = self.iconImageViewHolder.frame.size.width / 2 |
||||
self.iconImageViewHolder.layer.shadowPath = UIBezierPath(roundedRect: self.iconImageViewHolder.bounds, cornerRadius: self.iconImageViewHolder.layer.cornerRadius).cgPath |
||||
} |
||||
} |
||||
|
||||
@objc private func onTappedAdd() { |
||||
guard let dapp = viewModel?.dapp else { return } |
||||
delegate?.onAdd(dapp: dapp, inCell: self) |
||||
} |
||||
|
||||
@objc private func onTappedRemove() { |
||||
guard let dapp = viewModel?.dapp else { return } |
||||
delegate?.onRemove(dapp: dapp, inCell: self) |
||||
} |
||||
} |
@ -0,0 +1,79 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
|
||||
class MyDappCell: UITableViewCell { |
||||
|
||||
static let identifier = "MyDappCell" |
||||
|
||||
private let iconImageViewHolder = UIView() |
||||
private var viewModel: MyDappCellViewModel? |
||||
|
||||
let iconImageView = UIImageView() |
||||
let titleLabel = UILabel() |
||||
let urlLabel = UILabel() |
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { |
||||
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) |
||||
let labelsVerticalStackView = [ |
||||
titleLabel, |
||||
urlLabel].asStackView(axis: .vertical) |
||||
|
||||
iconImageView.translatesAutoresizingMaskIntoConstraints = false |
||||
iconImageViewHolder.addSubview(iconImageView) |
||||
|
||||
let mainStackView = [.spacerWidth(29), iconImageViewHolder, .spacerWidth(26), labelsVerticalStackView, .spacerWidth(29)].asStackView(axis: .horizontal, alignment: .center) |
||||
mainStackView.translatesAutoresizingMaskIntoConstraints = false |
||||
contentView.addSubview(mainStackView) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
mainStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), |
||||
mainStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), |
||||
mainStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 7), |
||||
mainStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -7), |
||||
|
||||
iconImageView.widthAnchor.constraint(equalToConstant: 44), |
||||
iconImageView.widthAnchor.constraint(equalTo: iconImageView.heightAnchor), |
||||
|
||||
iconImageView.leadingAnchor.constraint(equalTo: iconImageViewHolder.leadingAnchor), |
||||
iconImageView.trailingAnchor.constraint(equalTo: iconImageViewHolder.trailingAnchor), |
||||
iconImageView.topAnchor.constraint(equalTo: iconImageViewHolder.topAnchor), |
||||
iconImageView.bottomAnchor.constraint(equalTo: iconImageViewHolder.bottomAnchor), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: MyDappCellViewModel) { |
||||
self.viewModel = viewModel |
||||
|
||||
titleLabel.font = viewModel.nameFont |
||||
titleLabel.textColor = viewModel.nameColor |
||||
titleLabel.text = viewModel.name |
||||
|
||||
urlLabel.font = viewModel.domainNameFont |
||||
urlLabel.textColor = viewModel.domainNameColor |
||||
urlLabel.text = viewModel.domainName |
||||
|
||||
iconImageViewHolder.layer.shadowColor = viewModel.imageViewShadowColor.cgColor |
||||
iconImageViewHolder.layer.shadowOffset = viewModel.imageViewShadowOffset |
||||
iconImageViewHolder.layer.shadowOpacity = viewModel.imageViewShadowOpacity |
||||
iconImageViewHolder.layer.shadowRadius = viewModel.imageViewShadowRadius |
||||
|
||||
iconImageView.backgroundColor = viewModel.backgroundColor |
||||
iconImageView.contentMode = .scaleAspectFill |
||||
iconImageView.clipsToBounds = true |
||||
iconImageView.kf.setImage(with: viewModel.imageUrl, placeholder: viewModel.fallbackImage) |
||||
|
||||
//TODO ugly hack to get the image view's frame. Can't figure out a good point to retrieve the correct frame otherwise |
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { |
||||
self.iconImageView.layer.cornerRadius = self.iconImageView.frame.size.width / 2 |
||||
self.iconImageViewHolder.layer.cornerRadius = self.iconImageViewHolder.frame.size.width / 2 |
||||
self.iconImageViewHolder.layer.shadowPath = UIBezierPath(roundedRect: self.iconImageViewHolder.bounds, cornerRadius: self.iconImageViewHolder.layer.cornerRadius).cgPath |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,75 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
protocol MyDappsViewControllerHeaderViewDelegate: class { |
||||
func didEnterEditMode(inHeaderView headerView: MyDappsViewControllerHeaderView) |
||||
func didExitEditMode(inHeaderView headerView: MyDappsViewControllerHeaderView) |
||||
} |
||||
|
||||
class MyDappsViewControllerHeaderView: UIView { |
||||
private let header = DappsHomeHeaderView() |
||||
private let toggleEditModeButton = UIButton(type: .system) |
||||
private var isEditing = false { |
||||
didSet { |
||||
if isEditing { |
||||
configure(viewModel: .init(title: viewModel?.title ?? "")) |
||||
delegate?.didEnterEditMode(inHeaderView: self) |
||||
} else { |
||||
configure(viewModel: .init(title: viewModel?.title ?? "")) |
||||
delegate?.didExitEditMode(inHeaderView: self) |
||||
} |
||||
} |
||||
} |
||||
private var viewModel: DappsHomeHeaderViewViewModel? |
||||
|
||||
weak var delegate: MyDappsViewControllerHeaderViewDelegate? |
||||
|
||||
init() { |
||||
super.init(frame: .zero) |
||||
|
||||
header.translatesAutoresizingMaskIntoConstraints = false |
||||
addSubview(header) |
||||
|
||||
toggleEditModeButton.addTarget(self, action: #selector(toggleEditMode), for: .touchUpInside) |
||||
toggleEditModeButton.translatesAutoresizingMaskIntoConstraints = false |
||||
addSubview(toggleEditModeButton) |
||||
|
||||
NSLayoutConstraint.activate([ |
||||
header.trailingAnchor.constraint(equalTo: trailingAnchor), |
||||
header.leadingAnchor.constraint(equalTo: leadingAnchor), |
||||
header.topAnchor.constraint(equalTo: topAnchor, constant: 50), |
||||
header.bottomAnchor.constraint(equalTo: toggleEditModeButton.topAnchor, constant: -30), |
||||
|
||||
toggleEditModeButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), |
||||
toggleEditModeButton.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20), |
||||
]) |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
func configure(viewModel: DappsHomeHeaderViewViewModel) { |
||||
self.viewModel = viewModel |
||||
|
||||
backgroundColor = viewModel.backgroundColor |
||||
header.configure(viewModel: viewModel) |
||||
|
||||
if isEditing { |
||||
toggleEditModeButton.setTitle(R.string.localizable.done().localizedUppercase, for: .normal) |
||||
} else { |
||||
toggleEditModeButton.setTitle(R.string.localizable.editButtonTitle().localizedUppercase, for: .normal) |
||||
} |
||||
toggleEditModeButton.titleLabel?.font = Fonts.bold(size: 12) |
||||
} |
||||
|
||||
@objc private func toggleEditMode() { |
||||
isEditing = !isEditing |
||||
} |
||||
|
||||
func exitEditMode() { |
||||
isEditing = false |
||||
} |
||||
} |
@ -0,0 +1,47 @@ |
||||
// Copyright © 2018 Stormbird PTE. LTD. |
||||
|
||||
import Foundation |
||||
import UIKit |
||||
|
||||
///Wrap a child view and add insets around it. Useful for UITableView header/footers |
||||
class BoxView<T: UIView>: UIView { |
||||
let view: T |
||||
private var leftConstraint: NSLayoutConstraint? |
||||
private var rightConstraint: NSLayoutConstraint? |
||||
private var topConstraint: NSLayoutConstraint? |
||||
private var bottomConstraint: NSLayoutConstraint? |
||||
|
||||
var insets: UIEdgeInsets { |
||||
didSet { |
||||
updateConstraintsConstants() |
||||
} |
||||
} |
||||
|
||||
init(view: T, insets: UIEdgeInsets = .zero) { |
||||
self.view = view |
||||
self.insets = insets |
||||
super.init(frame: .zero) |
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false |
||||
addSubview(view) |
||||
leftConstraint = view.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0) |
||||
rightConstraint = trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0) |
||||
topConstraint = view.topAnchor.constraint(equalTo: topAnchor, constant: 0) |
||||
bottomConstraint = bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0) |
||||
if let leftConstraint = leftConstraint, let rightConstraint = rightConstraint, let topConstraint = topConstraint, let bottomConstraint = bottomConstraint { |
||||
NSLayoutConstraint.activate([leftConstraint, rightConstraint, topConstraint, bottomConstraint]) |
||||
} |
||||
updateConstraintsConstants() |
||||
} |
||||
|
||||
required init?(coder aDecoder: NSCoder) { |
||||
fatalError("init(coder:) has not been implemented") |
||||
} |
||||
|
||||
private func updateConstraintsConstants() { |
||||
leftConstraint?.constant = insets.left |
||||
rightConstraint?.constant = insets.right |
||||
topConstraint?.constant = insets.top |
||||
bottomConstraint?.constant = insets.bottom |
||||
} |
||||
} |