Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
5
iphone/Maps/.gitignore
vendored
Normal file
5
iphone/Maps/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Pods
|
||||
app.comaps.debug
|
||||
app.comaps.test
|
||||
app.comaps
|
||||
UI/Storyboard/Welcome.storyboard
|
||||
80
iphone/Maps/.swift-format
Normal file
80
iphone/Maps/.swift-format
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
{
|
||||
"fileScopedDeclarationPrivacy" : {
|
||||
"accessLevel" : "private"
|
||||
},
|
||||
"indentBlankLines" : true,
|
||||
"indentConditionalCompilationBlocks" : false,
|
||||
"indentSwitchCaseLabels" : true,
|
||||
"indentation" : {
|
||||
"spaces" : 4
|
||||
},
|
||||
"lineBreakAroundMultilineExpressionChainComponents" : false,
|
||||
"lineBreakBeforeControlFlowKeywords" : false,
|
||||
"lineBreakBeforeEachArgument" : false,
|
||||
"lineBreakBeforeEachGenericRequirement" : false,
|
||||
"lineBreakBetweenDeclarationAttributes" : false,
|
||||
"lineLength" : 1000,
|
||||
"maximumBlankLines" : 3,
|
||||
"multiElementCollectionTrailingCommas" : false,
|
||||
"noAssignmentInExpressions" : {
|
||||
"allowedFunctions" : [
|
||||
"XCTAssertNoThrow"
|
||||
]
|
||||
},
|
||||
"prioritizeKeepingFunctionOutputTogether" : false,
|
||||
"reflowMultilineStringLiterals" : {
|
||||
"never" : {
|
||||
|
||||
}
|
||||
},
|
||||
"respectsExistingLineBreaks" : true,
|
||||
"rules" : {
|
||||
"AllPublicDeclarationsHaveDocumentation" : false,
|
||||
"AlwaysUseLiteralForEmptyCollectionInit" : false,
|
||||
"AlwaysUseLowerCamelCase" : true,
|
||||
"AmbiguousTrailingClosureOverload" : true,
|
||||
"AvoidRetroactiveConformances" : true,
|
||||
"BeginDocumentationCommentWithOneLineSummary" : false,
|
||||
"DoNotUseSemicolons" : true,
|
||||
"DontRepeatTypeInStaticProperties" : true,
|
||||
"FileScopedDeclarationPrivacy" : true,
|
||||
"FullyIndirectEnum" : true,
|
||||
"GroupNumericLiterals" : true,
|
||||
"IdentifiersMustBeASCII" : true,
|
||||
"NeverForceUnwrap" : false,
|
||||
"NeverUseForceTry" : false,
|
||||
"NeverUseImplicitlyUnwrappedOptionals" : false,
|
||||
"NoAccessLevelOnExtensionDeclaration" : true,
|
||||
"NoAssignmentInExpressions" : true,
|
||||
"NoBlockComments" : true,
|
||||
"NoCasesWithOnlyFallthrough" : true,
|
||||
"NoEmptyLinesOpeningClosingBraces" : false,
|
||||
"NoEmptyTrailingClosureParentheses" : true,
|
||||
"NoLabelsInCasePatterns" : true,
|
||||
"NoLeadingUnderscores" : false,
|
||||
"NoParensAroundConditions" : true,
|
||||
"NoPlaygroundLiterals" : true,
|
||||
"NoVoidReturnOnFunctionSignature" : true,
|
||||
"OmitExplicitReturns" : false,
|
||||
"OneCasePerLine" : true,
|
||||
"OneVariableDeclarationPerLine" : true,
|
||||
"OnlyOneTrailingClosureArgument" : true,
|
||||
"OrderedImports" : true,
|
||||
"ReplaceForEachWithForLoop" : true,
|
||||
"ReturnVoidInsteadOfEmptyTuple" : true,
|
||||
"TypeNamesShouldBeCapitalized" : true,
|
||||
"UseEarlyExits" : false,
|
||||
"UseExplicitNilCheckInConditions" : true,
|
||||
"UseLetInEveryBoundCaseVariable" : true,
|
||||
"UseShorthandTypeNames" : true,
|
||||
"UseSingleLinePropertyGetter" : true,
|
||||
"UseSynthesizedInitializer" : true,
|
||||
"UseTripleSlashForDocumentationComments" : true,
|
||||
"UseWhereClausesInForLoops" : false,
|
||||
"ValidateDocumentationComments" : false
|
||||
},
|
||||
"spacesAroundRangeFormationOperators" : false,
|
||||
"spacesBeforeEndOfLineComments" : 2,
|
||||
"tabWidth" : 4,
|
||||
"version" : 1
|
||||
}
|
||||
BIN
iphone/Maps/Alert 5.m4a
Normal file
BIN
iphone/Maps/Alert 5.m4a
Normal file
Binary file not shown.
85
iphone/Maps/Bookmarks/BookmarksCoordinator.swift
Normal file
85
iphone/Maps/Bookmarks/BookmarksCoordinator.swift
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import UIKit
|
||||
|
||||
@objc class BookmarksCoordinator: NSObject {
|
||||
enum BookmarksState {
|
||||
case opened
|
||||
case closed
|
||||
case hidden(categoryId: MWMMarkGroupID)
|
||||
}
|
||||
|
||||
private weak var navigationController: UINavigationController?
|
||||
private weak var controlsManager: MWMMapViewControlsManager?
|
||||
private weak var navigationManager: MWMNavigationDashboardManager?
|
||||
private var bookmarksControllers: [UIViewController]?
|
||||
private var state: BookmarksState = .closed {
|
||||
didSet {
|
||||
updateForState(newState: state)
|
||||
}
|
||||
}
|
||||
|
||||
@objc init(navigationController: UINavigationController,
|
||||
controlsManager: MWMMapViewControlsManager,
|
||||
navigationManager: MWMNavigationDashboardManager) {
|
||||
self.navigationController = navigationController
|
||||
self.controlsManager = controlsManager
|
||||
self.navigationManager = navigationManager
|
||||
}
|
||||
|
||||
@objc func open() {
|
||||
state = .opened
|
||||
}
|
||||
|
||||
@objc func close() {
|
||||
state = .closed
|
||||
}
|
||||
|
||||
@objc func goBack() {
|
||||
switch state {
|
||||
case .opened:
|
||||
navigationController?.popViewController(animated: true)
|
||||
case .hidden, .closed:
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func hide(categoryId: MWMMarkGroupID) {
|
||||
state = .hidden(categoryId: categoryId)
|
||||
}
|
||||
|
||||
private func updateForState(newState: BookmarksState) {
|
||||
guard let navigationController = navigationController else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
switch state {
|
||||
case .opened:
|
||||
guard let bookmarksControllers = bookmarksControllers else {
|
||||
// Instead of BookmarksTabViewController
|
||||
let bookmarks = BMCViewController(coordinator: self)
|
||||
bookmarks.title = L("bookmarks_and_tracks")
|
||||
navigationController.pushViewController(bookmarks, animated: true)
|
||||
return
|
||||
}
|
||||
var controllers = navigationController.viewControllers
|
||||
controllers.append(contentsOf: bookmarksControllers)
|
||||
UIView.transition(with: self.navigationController!.view,
|
||||
duration: kDefaultAnimationDuration,
|
||||
options: [.curveEaseInOut, .transitionCrossDissolve],
|
||||
animations: {
|
||||
navigationController.setViewControllers(controllers, animated: false)
|
||||
}, completion: nil)
|
||||
FrameworkHelper.deactivateMapSelection()
|
||||
self.bookmarksControllers = nil
|
||||
case .closed:
|
||||
navigationController.popToRootViewController(animated: true)
|
||||
bookmarksControllers = nil
|
||||
case .hidden(_):
|
||||
UIView.transition(with: self.navigationController!.view,
|
||||
duration: kDefaultAnimationDuration,
|
||||
options: [.curveEaseInOut, .transitionCrossDissolve],
|
||||
animations: {
|
||||
self.bookmarksControllers = navigationController.popToRootViewController(animated: false)
|
||||
}, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
final class BookmarksListBuilder {
|
||||
static func build(markGroupId: MWMMarkGroupID,
|
||||
bookmarksCoordinator: BookmarksCoordinator?,
|
||||
delegate: BookmarksListDelegate? = nil) -> BookmarksListViewController {
|
||||
let viewController = BookmarksListViewController()
|
||||
let router = BookmarksListRouter(MapViewController.shared()!, bookmarksCoordinator: bookmarksCoordinator)
|
||||
let interactor = BookmarksListInteractor(markGroupId: markGroupId)
|
||||
let presenter = BookmarksListPresenter(view: viewController,
|
||||
router: router,
|
||||
delegate: delegate,
|
||||
interactor: interactor)
|
||||
viewController.presenter = presenter
|
||||
return viewController
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
protocol BookmarksListInfoViewControllerDelegate: AnyObject {
|
||||
func didPressDescription()
|
||||
func didUpdateContent()
|
||||
}
|
||||
|
||||
final class BookmarksListInfoViewController: UIViewController {
|
||||
var info: IBookmarksListInfoViewModel? {
|
||||
didSet {
|
||||
guard isViewLoaded, let info = info else { return }
|
||||
updateInfo(info)
|
||||
}
|
||||
}
|
||||
|
||||
weak var delegate: BookmarksListInfoViewControllerDelegate?
|
||||
|
||||
@IBOutlet private var titleImageView: UIImageView!
|
||||
@IBOutlet private var titleLabel: UILabel!
|
||||
@IBOutlet private var descriptionButton: UIButton!
|
||||
@IBOutlet private var authorContainerView: UIView!
|
||||
@IBOutlet private var infoStack: UIStackView!
|
||||
@IBOutlet private var separatorsConstraints: [NSLayoutConstraint]!
|
||||
|
||||
@IBAction private func onDescription(_ sender: UIButton) {
|
||||
delegate?.didPressDescription()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
separatorsConstraints.forEach { $0.constant = 1 / UIScreen.main.scale }
|
||||
descriptionButton.titleLabel?.numberOfLines = 2
|
||||
|
||||
guard let info = info else { return }
|
||||
updateInfo(info)
|
||||
}
|
||||
|
||||
private func updateInfo(_ info: IBookmarksListInfoViewModel) {
|
||||
titleLabel.text = info.title
|
||||
descriptionButton.isHidden = !info.hasDescription
|
||||
if info.hasDescription {
|
||||
let description = info.isHtmlDescription
|
||||
? BookmarksListInfoViewController.getPlainText(info.description)
|
||||
: info.description
|
||||
descriptionButton.setTitle(description, for: .normal)
|
||||
}
|
||||
|
||||
titleImageView.isHidden = true
|
||||
if let imageUrl = info.imageUrl {
|
||||
titleImageView.wi_setImage(with: imageUrl, transitionDuration: 0) { [weak self] (image, error) in
|
||||
guard image != nil else { return }
|
||||
self?.titleImageView.isHidden = false
|
||||
self?.delegate?.didUpdateContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func getPlainText(_ htmlText: String) -> String? {
|
||||
let formattedText = NSAttributedString.string(withHtml: htmlText, defaultAttributes: [:])
|
||||
return formattedText?.string
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="BookmarksListInfoViewController" customModule="CoMaps" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="authorContainerView" destination="bit-FN-ytL" id="CAI-TP-A5D"/>
|
||||
<outlet property="descriptionButton" destination="cxc-pt-qei" id="zce-so-aHF"/>
|
||||
<outlet property="infoStack" destination="226-gO-8GN" id="f2A-eJ-FGG"/>
|
||||
<outlet property="titleImageView" destination="dsB-Xh-jOl" id="rsb-lH-MDN"/>
|
||||
<outlet property="titleLabel" destination="TFR-Qj-pUF" id="D2T-y8-ztA"/>
|
||||
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
||||
<outletCollection property="separatorsConstraints" destination="GqW-4j-7uM" collectionClass="NSMutableArray" id="wXt-eX-9BJ"/>
|
||||
<outletCollection property="separatorsConstraints" destination="soU-hC-iDQ" collectionClass="NSMutableArray" id="bQl-DL-hUh"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="340"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="KGV-R1-hoS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="340"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dsB-Xh-jOl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="180"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="180" id="9eu-ij-E7g"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jGS-bD-wyb">
|
||||
<rect key="frame" x="0.0" y="180" width="375" height="159"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="226-gO-8GN">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="159"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Z9F-Yb-cDq">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="48"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Paris" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TFR-Qj-pUF">
|
||||
<rect key="frame" x="0.0" y="16" width="343" height="32"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="24"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="bold24:blackPrimaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="TFR-Qj-pUF" firstAttribute="top" secondItem="Z9F-Yb-cDq" secondAttribute="top" constant="16" id="1Pq-uJ-IXO"/>
|
||||
<constraint firstAttribute="trailing" secondItem="TFR-Qj-pUF" secondAttribute="trailing" id="9jE-YY-ZE7"/>
|
||||
<constraint firstItem="TFR-Qj-pUF" firstAttribute="leading" secondItem="Z9F-Yb-cDq" secondAttribute="leading" id="Mqn-yA-iWF"/>
|
||||
<constraint firstAttribute="bottom" secondItem="TFR-Qj-pUF" secondAttribute="bottom" id="j5w-Th-flQ"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="leading" contentVerticalAlignment="center" buttonType="system" lineBreakMode="tailTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="cxc-pt-qei">
|
||||
<rect key="frame" x="0.0" y="48" width="343" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="40" id="g6j-hs-K1V"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
|
||||
<state key="normal" title="DESCRIPTION"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="FlatNormalTransButton"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="onDescription:" destination="-1" eventType="touchUpInside" id="fQR-bp-Mds"/>
|
||||
</connections>
|
||||
</button>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TIy-PU-SzO">
|
||||
<rect key="frame" x="0.0" y="88" width="343" height="10"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="10" id="HNz-j1-2hp"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bit-FN-ytL">
|
||||
<rect key="frame" x="0.0" y="98" width="343" height="61"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="aEi-QI-Tol">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="1"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.11852525684931507" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="1" id="soU-hC-iDQ"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="Divider"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="aEi-QI-Tol" firstAttribute="leading" secondItem="bit-FN-ytL" secondAttribute="leading" id="bGS-V6-zTn"/>
|
||||
<constraint firstItem="aEi-QI-Tol" firstAttribute="top" secondItem="bit-FN-ytL" secondAttribute="top" id="jX7-lf-0c5"/>
|
||||
<constraint firstAttribute="trailing" secondItem="aEi-QI-Tol" secondAttribute="trailing" id="xWz-Ie-kOO"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="226-gO-8GN" secondAttribute="trailing" constant="16" id="Bc0-zj-luu"/>
|
||||
<constraint firstItem="226-gO-8GN" firstAttribute="leading" secondItem="jGS-bD-wyb" secondAttribute="leading" constant="16" id="CBz-2b-FdW"/>
|
||||
<constraint firstAttribute="bottom" secondItem="226-gO-8GN" secondAttribute="bottom" id="eCj-gO-9cQ"/>
|
||||
<constraint firstItem="226-gO-8GN" firstAttribute="top" secondItem="jGS-bD-wyb" secondAttribute="top" id="wIb-n9-mts"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="p5d-oU-wVm">
|
||||
<rect key="frame" x="0.0" y="339" width="375" height="1"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.11852525680000001" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="1" id="GqW-4j-7uM"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="Divider"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="KGV-R1-hoS" secondAttribute="bottom" id="Mds-pB-3uh"/>
|
||||
<constraint firstItem="KGV-R1-hoS" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="bKw-en-SqM"/>
|
||||
<constraint firstItem="KGV-R1-hoS" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="j8z-W0-Y6S"/>
|
||||
<constraint firstAttribute="trailing" secondItem="KGV-R1-hoS" secondAttribute="trailing" id="wv4-3Q-Ihc"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="Background"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<point key="canvasLocation" x="111.2" y="-128.63568215892056"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
extension BookmarksListSortingType {
|
||||
init(_ sortingType: BookmarksSortingType) {
|
||||
switch sortingType {
|
||||
case .byType:
|
||||
self = .type
|
||||
case .byDistance:
|
||||
self = .distance
|
||||
case .byTime:
|
||||
self = .date
|
||||
case .byName:
|
||||
self = .name
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class BookmarksListInteractor: NSObject {
|
||||
private var markGroupId: MWMMarkGroupID
|
||||
private var bookmarksManager: BookmarksManager
|
||||
|
||||
var onCategoryReload: ((GroupReloadingResult) -> Void)?
|
||||
|
||||
init(markGroupId: MWMMarkGroupID) {
|
||||
self.markGroupId = markGroupId
|
||||
self.bookmarksManager = BookmarksManager.shared()
|
||||
super.init()
|
||||
self.addToBookmarksManagerObserverList()
|
||||
}
|
||||
|
||||
deinit {
|
||||
removeFromBookmarksManagerObserverList()
|
||||
}
|
||||
}
|
||||
|
||||
extension BookmarksListInteractor: IBookmarksListInteractor {
|
||||
func getBookmarkGroup() -> BookmarkGroup {
|
||||
bookmarksManager.category(withId: markGroupId)
|
||||
}
|
||||
|
||||
func hasDescription() -> Bool {
|
||||
bookmarksManager.hasExtraInfo(markGroupId)
|
||||
}
|
||||
|
||||
func prepareForSearch() {
|
||||
bookmarksManager.prepare(forSearch: markGroupId)
|
||||
}
|
||||
|
||||
func search(_ text: String, completion: @escaping ([Bookmark]) -> Void) {
|
||||
bookmarksManager.searchBookmarksGroup(markGroupId, text: text) {
|
||||
completion($0)
|
||||
}
|
||||
}
|
||||
|
||||
func availableSortingTypes(hasMyPosition: Bool) -> [BookmarksListSortingType] {
|
||||
bookmarksManager.availableSortingTypes(markGroupId, hasMyPosition: hasMyPosition).map {
|
||||
BookmarksSortingType(rawValue: $0.intValue)!
|
||||
}.map {
|
||||
switch $0 {
|
||||
case .byType:
|
||||
return BookmarksListSortingType.type
|
||||
case .byDistance:
|
||||
return BookmarksListSortingType.distance
|
||||
case .byTime:
|
||||
return BookmarksListSortingType.date
|
||||
case .byName:
|
||||
return BookmarksListSortingType.name
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func viewOnMap() {
|
||||
FrameworkHelper.show(onMap: markGroupId)
|
||||
}
|
||||
|
||||
func viewBookmarkOnMap(_ bookmarkId: MWMMarkID) {
|
||||
FrameworkHelper.showBookmark(bookmarkId)
|
||||
}
|
||||
|
||||
func viewTrackOnMap(_ trackId: MWMTrackID) {
|
||||
FrameworkHelper.showTrack(trackId)
|
||||
}
|
||||
|
||||
func setGroup(_ groupId: MWMMarkGroupID, visible: Bool) {
|
||||
bookmarksManager.setCategory(groupId, isVisible: visible)
|
||||
}
|
||||
|
||||
func sort(_ sortingType: BookmarksListSortingType,
|
||||
location: CLLocation?,
|
||||
completion: @escaping ([BookmarksSection]) -> Void) {
|
||||
let coreSortingType: BookmarksSortingType
|
||||
switch sortingType {
|
||||
case .distance:
|
||||
coreSortingType = .byDistance
|
||||
case .date:
|
||||
coreSortingType = .byTime
|
||||
case .type:
|
||||
coreSortingType = .byType
|
||||
case .name:
|
||||
coreSortingType = .byName
|
||||
}
|
||||
|
||||
bookmarksManager.sortBookmarks(markGroupId,
|
||||
sortingType: coreSortingType,
|
||||
location: location) { sections in
|
||||
guard let sections = sections else { return }
|
||||
completion(sections)
|
||||
}
|
||||
}
|
||||
|
||||
func resetSort() {
|
||||
bookmarksManager.resetLastSortingType(markGroupId)
|
||||
}
|
||||
|
||||
func lastSortingType() -> BookmarksListSortingType? {
|
||||
guard bookmarksManager.hasLastSortingType(markGroupId) else {
|
||||
return nil
|
||||
}
|
||||
return BookmarksListSortingType(bookmarksManager.lastSortingType(markGroupId))
|
||||
}
|
||||
|
||||
func deleteBookmark(_ bookmarkId: MWMMarkID) {
|
||||
bookmarksManager.deleteBookmark(bookmarkId)
|
||||
}
|
||||
|
||||
func deleteTrack(_ trackId: MWMTrackID) {
|
||||
bookmarksManager.deleteTrack(trackId)
|
||||
}
|
||||
|
||||
func moveBookmark(_ bookmarkId: MWMMarkID, toGroupId groupId: MWMMarkGroupID) {
|
||||
bookmarksManager.moveBookmark(bookmarkId, toGroupId: groupId)
|
||||
}
|
||||
|
||||
func moveTrack(_ trackId: MWMTrackID, toGroupId groupId: MWMMarkGroupID) {
|
||||
bookmarksManager.moveTrack(trackId, toGroupId: groupId)
|
||||
}
|
||||
|
||||
func updateBookmark(_ bookmarkId: MWMMarkID, setGroupId groupId: MWMMarkGroupID, title: String, color: BookmarkColor, description: String) {
|
||||
bookmarksManager.updateBookmark(bookmarkId, setGroupId: groupId, title: title, color: color, description: description)
|
||||
}
|
||||
|
||||
func updateTrack(_ trackId: MWMTrackID, setGroupId groupId: MWMMarkGroupID) {
|
||||
bookmarksManager.moveTrack(trackId, toGroupId: groupId)
|
||||
}
|
||||
|
||||
func deleteBookmarksGroup() {
|
||||
bookmarksManager.deleteCategory(markGroupId)
|
||||
}
|
||||
|
||||
func canDeleteGroup() -> Bool {
|
||||
bookmarksManager.userCategoriesCount() > 1
|
||||
}
|
||||
|
||||
func exportFile(fileType: KmlFileType, completion: @escaping SharingResultCompletionHandler) {
|
||||
bookmarksManager.shareCategory(markGroupId, fileType: fileType, completion: completion)
|
||||
}
|
||||
|
||||
func finishExportFile() {
|
||||
bookmarksManager.finishSharing()
|
||||
}
|
||||
|
||||
func addToBookmarksManagerObserverList() {
|
||||
bookmarksManager.add(self)
|
||||
}
|
||||
|
||||
func removeFromBookmarksManagerObserverList() {
|
||||
bookmarksManager.remove(self)
|
||||
}
|
||||
|
||||
func reloadCategory() {
|
||||
onCategoryReload?(bookmarksManager.hasCategory(markGroupId) ? .success : .notFound)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BookmarksObserver
|
||||
extension BookmarksListInteractor: BookmarksObserver {
|
||||
func onBookmarksLoadFinished() {
|
||||
reloadCategory()
|
||||
}
|
||||
|
||||
func onBookmarksCategoryDeleted(_ groupId: MWMMarkGroupID) {
|
||||
reloadCategory()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import Foundation
|
||||
|
||||
enum BookmarksListVisibilityButtonState {
|
||||
case hidden
|
||||
case hideAll
|
||||
case showAll
|
||||
}
|
||||
|
||||
enum BookmarkToolbarButtonSource {
|
||||
case sort
|
||||
case more
|
||||
}
|
||||
|
||||
enum GroupReloadingResult {
|
||||
case success
|
||||
case notFound
|
||||
}
|
||||
|
||||
protocol IBookmarksListSectionViewModel {
|
||||
var numberOfItems: Int { get }
|
||||
var sectionTitle: String { get }
|
||||
var visibilityButtonState: BookmarksListVisibilityButtonState { get }
|
||||
var canEdit: Bool { get }
|
||||
}
|
||||
|
||||
protocol IBookmarksSectionViewModel: IBookmarksListSectionViewModel {
|
||||
var bookmarks: [IBookmarksListItemViewModel] { get }
|
||||
}
|
||||
|
||||
protocol ITracksSectionViewModel: IBookmarksListSectionViewModel {
|
||||
var tracks: [IBookmarksListItemViewModel] { get }
|
||||
}
|
||||
|
||||
protocol ISubgroupsSectionViewModel: IBookmarksListSectionViewModel {
|
||||
var subgroups: [ISubgroupViewModel] { get }
|
||||
var type: BookmarkGroupType { get }
|
||||
}
|
||||
|
||||
protocol IBookmarksListItemViewModel {
|
||||
var name: String { get }
|
||||
var subtitle: String { get }
|
||||
var image: UIImage { get }
|
||||
var colorDidTapAction: (() -> Void)? { get }
|
||||
}
|
||||
|
||||
protocol ISubgroupViewModel {
|
||||
var subgroupName: String { get }
|
||||
var subtitle: String { get }
|
||||
var isVisible: Bool { get }
|
||||
}
|
||||
|
||||
protocol IBookmarksListMenuItem {
|
||||
var title: String { get }
|
||||
var destructive: Bool { get }
|
||||
var enabled: Bool { get }
|
||||
var action: () -> Void { get }
|
||||
}
|
||||
|
||||
protocol IBookmarksListView: AnyObject {
|
||||
func setTitle(_ title: String)
|
||||
func setInfo(_ info: IBookmarksListInfoViewModel)
|
||||
func setSections(_ sections: [IBookmarksListSectionViewModel])
|
||||
func setMoreItemTitle(_ itemTitle: String)
|
||||
func showMenu(_ items: [IBookmarksListMenuItem], from source: BookmarkToolbarButtonSource)
|
||||
func showColorPicker(with pickerType: ColorPickerType, _ completion: ((UIColor) -> Void)?)
|
||||
func enableEditing(_ enable: Bool)
|
||||
func share(_ url: URL, completion: @escaping () -> Void)
|
||||
func showError(title: String, message: String)
|
||||
}
|
||||
|
||||
protocol IBookmarksListPresenter {
|
||||
func viewDidLoad()
|
||||
func viewDidAppear()
|
||||
func activateSearch()
|
||||
func deactivateSearch()
|
||||
func cancelSearch()
|
||||
func search(_ text: String)
|
||||
func sort()
|
||||
func more()
|
||||
func deleteItem(in section: IBookmarksListSectionViewModel, at index: Int)
|
||||
func moveItem(in section: IBookmarksListSectionViewModel, at index: Int)
|
||||
func editItem(in section: IBookmarksListSectionViewModel, at index: Int)
|
||||
func selectItem(in section: IBookmarksListSectionViewModel, at index: Int)
|
||||
func checkItem(in section: IBookmarksListSectionViewModel, at index: Int, checked: Bool)
|
||||
func toggleVisibility(in section: IBookmarksListSectionViewModel)
|
||||
func showDescription()
|
||||
}
|
||||
|
||||
enum BookmarksListSortingType {
|
||||
case distance
|
||||
case date
|
||||
case type
|
||||
case name
|
||||
}
|
||||
|
||||
protocol IBookmarksListInteractor {
|
||||
var onCategoryReload: ((GroupReloadingResult) -> Void)? { get set }
|
||||
|
||||
func getBookmarkGroup() -> BookmarkGroup
|
||||
func hasDescription() -> Bool
|
||||
func prepareForSearch()
|
||||
func search(_ text: String, completion: @escaping ([Bookmark]) -> Void)
|
||||
func availableSortingTypes(hasMyPosition: Bool) -> [BookmarksListSortingType]
|
||||
func viewOnMap()
|
||||
func viewBookmarkOnMap(_ bookmarkId: MWMMarkID)
|
||||
func viewTrackOnMap(_ trackId: MWMTrackID)
|
||||
func setGroup(_ groupId: MWMMarkGroupID, visible: Bool)
|
||||
func sort(_ sortingType: BookmarksListSortingType,
|
||||
location: CLLocation?,
|
||||
completion: @escaping ([BookmarksSection]) -> Void)
|
||||
func resetSort()
|
||||
func lastSortingType() -> BookmarksListSortingType?
|
||||
func deleteBookmark(_ bookmarkId: MWMMarkID)
|
||||
func deleteTrack(_ trackId: MWMTrackID)
|
||||
func moveBookmark(_ bookmarkId: MWMMarkID, toGroupId: MWMMarkGroupID)
|
||||
func moveTrack(_ trackId: MWMTrackID, toGroupId: MWMMarkGroupID)
|
||||
func updateBookmark(_ bookmarkId: MWMMarkID, setGroupId groupId: MWMMarkGroupID, title: String, color: BookmarkColor, description: String)
|
||||
func updateTrack(_ trackId: MWMTrackID, setGroupId groupId: MWMMarkGroupID)
|
||||
func deleteBookmarksGroup()
|
||||
func canDeleteGroup() -> Bool
|
||||
func exportFile(fileType: KmlFileType, completion: @escaping SharingResultCompletionHandler)
|
||||
func finishExportFile()
|
||||
}
|
||||
|
||||
protocol IBookmarksListRouter {
|
||||
func listSettings(_ bookmarkGroup: BookmarkGroup, delegate: CategorySettingsViewControllerDelegate?)
|
||||
func viewOnMap(_ bookmarkGroup: BookmarkGroup)
|
||||
func showDescription(_ bookmarkGroup: BookmarkGroup)
|
||||
func showSubgroup(_ subgroupId: MWMMarkGroupID)
|
||||
func selectGroup(currentGroupName groupName: String,
|
||||
currentGroupId groupId: MWMMarkGroupID,
|
||||
delegate: SelectBookmarkGroupViewControllerDelegate?)
|
||||
func editBookmark(bookmarkId: MWMMarkID, completion: @escaping (Bool) -> Void)
|
||||
func editTrack(trackId: MWMTrackID, completion: @escaping (Bool) -> Void)
|
||||
func goBack()
|
||||
}
|
||||
|
||||
protocol IBookmarksListInfoViewModel {
|
||||
var title: String { get }
|
||||
var description: String { get }
|
||||
var hasDescription: Bool { get }
|
||||
var isHtmlDescription: Bool { get }
|
||||
var imageUrl: URL? { get }
|
||||
}
|
||||
562
iphone/Maps/Bookmarks/BookmarksList/BookmarksListPresenter.swift
Normal file
562
iphone/Maps/Bookmarks/BookmarksList/BookmarksListPresenter.swift
Normal file
|
|
@ -0,0 +1,562 @@
|
|||
protocol BookmarksListDelegate: AnyObject {
|
||||
func bookmarksListDidDeleteGroup()
|
||||
}
|
||||
|
||||
final class BookmarksListPresenter {
|
||||
private unowned let view: IBookmarksListView
|
||||
private let router: IBookmarksListRouter
|
||||
private var interactor: IBookmarksListInteractor
|
||||
private weak var delegate: BookmarksListDelegate?
|
||||
private var bookmarkGroup: BookmarkGroup
|
||||
|
||||
private enum EditableItem {
|
||||
case bookmark(MWMMarkID)
|
||||
case track(MWMTrackID)
|
||||
}
|
||||
private var editingItem: EditableItem?
|
||||
|
||||
init(view: IBookmarksListView,
|
||||
router: IBookmarksListRouter,
|
||||
delegate: BookmarksListDelegate?,
|
||||
interactor: IBookmarksListInteractor) {
|
||||
self.view = view
|
||||
self.router = router
|
||||
self.delegate = delegate
|
||||
self.interactor = interactor
|
||||
self.bookmarkGroup = interactor.getBookmarkGroup()
|
||||
self.subscribeOnGroupReloading()
|
||||
}
|
||||
|
||||
private func subscribeOnGroupReloading() {
|
||||
interactor.onCategoryReload = { [weak self] result in
|
||||
guard let self else { return }
|
||||
switch result {
|
||||
case .notFound:
|
||||
self.router.goBack()
|
||||
case .success:
|
||||
self.bookmarkGroup = self.interactor.getBookmarkGroup()
|
||||
self.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reload() {
|
||||
guard let sortingType = interactor.lastSortingType() else {
|
||||
setDefaultSections()
|
||||
return
|
||||
}
|
||||
sort(sortingType)
|
||||
}
|
||||
|
||||
private func setDefaultSections() {
|
||||
interactor.resetSort()
|
||||
var sections: [IBookmarksListSectionViewModel] = []
|
||||
let tracks = bookmarkGroup.tracks.map { track in
|
||||
TrackViewModel(track, formattedDistance: formatDistance(Double(track.trackLengthMeters)), colorDidTap: {
|
||||
self.view.showColorPicker(with: .defaultColorPicker(track.trackColor)) { color in
|
||||
BookmarksManager.shared().updateTrack(track.trackId, setColor: color)
|
||||
self.reload()
|
||||
}
|
||||
})
|
||||
}
|
||||
if !tracks.isEmpty {
|
||||
sections.append(TracksSectionViewModel(tracks: tracks))
|
||||
}
|
||||
|
||||
let collections = bookmarkGroup.collections.map { SubgroupViewModel($0) }
|
||||
if !collections.isEmpty {
|
||||
sections.append(SubgroupsSectionViewModel(title: L("collections"), subgroups: collections, type: .collection))
|
||||
}
|
||||
|
||||
let categories = bookmarkGroup.categories.map { SubgroupViewModel($0)}
|
||||
if !categories.isEmpty {
|
||||
sections.append(SubgroupsSectionViewModel(title: L("categories"), subgroups: categories, type: .category))
|
||||
}
|
||||
|
||||
let bookmarks = mapBookmarks(bookmarkGroup.bookmarks)
|
||||
if !bookmarks.isEmpty {
|
||||
sections.append(BookmarksSectionViewModel(title: L("bookmarks"), bookmarks: bookmarks))
|
||||
}
|
||||
view.setSections(sections)
|
||||
}
|
||||
|
||||
private func mapBookmarks(_ bookmarks: [Bookmark]) -> [BookmarkViewModel] {
|
||||
let location = LocationManager.lastLocation()
|
||||
return bookmarks.map { bookmark in
|
||||
let formattedDistance: String?
|
||||
if let location = location {
|
||||
let distance = location.distance(from: CLLocation(latitude: bookmark.locationCoordinate.latitude,
|
||||
longitude: bookmark.locationCoordinate.longitude))
|
||||
formattedDistance = formatDistance(distance)
|
||||
} else {
|
||||
formattedDistance = nil
|
||||
}
|
||||
return BookmarkViewModel(bookmark, formattedDistance: formattedDistance, colorDidTap: { [weak self] in
|
||||
self?.view.showColorPicker(with: .bookmarkColorPicker(bookmark.bookmarkColor)) { color in
|
||||
guard let bookmarkColor = BookmarkColor.bookmarkColor(from: color) else { return }
|
||||
BookmarksManager.shared().updateBookmark(bookmark.bookmarkId, setColor: bookmarkColor)
|
||||
self?.reload()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func formatDistance(_ distance: Double) -> String {
|
||||
DistanceFormatter.distanceString(fromMeters: distance)
|
||||
}
|
||||
|
||||
private func showSortMenu() {
|
||||
var sortItems = interactor.availableSortingTypes(hasMyPosition: LocationManager.lastLocation() != nil)
|
||||
.map { sortingType -> BookmarksListMenuItem in
|
||||
switch sortingType {
|
||||
case .distance:
|
||||
return BookmarksListMenuItem(title: L("sort_distance"), action: { [weak self] in
|
||||
self?.sort(.distance)
|
||||
})
|
||||
case .date:
|
||||
return BookmarksListMenuItem(title: L("sort_date"), action: { [weak self] in
|
||||
self?.sort(.date)
|
||||
})
|
||||
case .type:
|
||||
return BookmarksListMenuItem(title: L("sort_type"), action: { [weak self] in
|
||||
self?.sort(.type)
|
||||
})
|
||||
case .name:
|
||||
return BookmarksListMenuItem(title: L("sort_name"), action: { [weak self] in
|
||||
self?.sort(.name)
|
||||
})
|
||||
}
|
||||
}
|
||||
sortItems.append(BookmarksListMenuItem(title: L("sort_default"), action: { [weak self] in
|
||||
self?.setDefaultSections()
|
||||
}))
|
||||
view.showMenu(sortItems, from: .sort)
|
||||
}
|
||||
|
||||
private func showMoreMenu() {
|
||||
var moreItems: [BookmarksListMenuItem] = []
|
||||
moreItems.append(BookmarksListMenuItem(title: L("search_show_on_map"), action: { [weak self] in
|
||||
self?.viewOnMap()
|
||||
}))
|
||||
moreItems.append(BookmarksListMenuItem(title: L("edit"), action: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.router.listSettings(self.bookmarkGroup, delegate: self)
|
||||
}))
|
||||
|
||||
func exportMenuItem(for fileType: KmlFileType) -> BookmarksListMenuItem {
|
||||
let title: String
|
||||
switch fileType {
|
||||
case .text:
|
||||
title = L("export_file")
|
||||
case .gpx:
|
||||
title = L("export_file_gpx")
|
||||
default:
|
||||
fatalError("Unexpected file type")
|
||||
}
|
||||
return BookmarksListMenuItem(title: title, action: { [weak self] in
|
||||
self?.interactor.exportFile(fileType: fileType) { (status, url) in
|
||||
switch status {
|
||||
case .success:
|
||||
guard let url = url else { fatalError() }
|
||||
self?.view.share(url) {
|
||||
self?.interactor.finishExportFile()
|
||||
}
|
||||
case .emptyCategory:
|
||||
self?.view.showError(title: L("bookmarks_error_title_share_empty"),
|
||||
message: L("bookmarks_error_message_share_empty"))
|
||||
case .archiveError, .fileError:
|
||||
self?.view.showError(title: L("dialog_routing_system_error"),
|
||||
message: L("bookmarks_error_message_share_general"))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
moreItems.append(exportMenuItem(for: .text))
|
||||
moreItems.append(exportMenuItem(for: .gpx))
|
||||
moreItems.append(BookmarksListMenuItem(title: L("delete_list"),
|
||||
destructive: true,
|
||||
enabled: interactor.canDeleteGroup(),
|
||||
action: { [weak self] in
|
||||
self?.interactor.deleteBookmarksGroup()
|
||||
self?.delegate?.bookmarksListDidDeleteGroup()
|
||||
}))
|
||||
view.showMenu(moreItems, from: .more)
|
||||
}
|
||||
|
||||
private func viewOnMap() {
|
||||
interactor.viewOnMap()
|
||||
router.viewOnMap(bookmarkGroup)
|
||||
}
|
||||
|
||||
private func sort(_ sortingType: BookmarksListSortingType) {
|
||||
interactor.sort(sortingType, location: LocationManager.lastLocation()) { [weak self] sortedSections in
|
||||
let sections = sortedSections.map { (bookmarksSection) -> IBookmarksListSectionViewModel in
|
||||
if let bookmarks = bookmarksSection.bookmarks, let self = self {
|
||||
return BookmarksSectionViewModel(title: bookmarksSection.sectionName, bookmarks: self.mapBookmarks(bookmarks))
|
||||
}
|
||||
if let tracks = bookmarksSection.tracks, let self = self {
|
||||
return TracksSectionViewModel(tracks: tracks.map { track in
|
||||
TrackViewModel(track, formattedDistance: self.formatDistance(Double(track.trackLengthMeters)), colorDidTap: {
|
||||
self.view.showColorPicker(with: .defaultColorPicker(track.trackColor)) { color in
|
||||
BookmarksManager.shared().updateTrack(track.trackId, setColor: color)
|
||||
self.reload()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
fatalError()
|
||||
}
|
||||
self?.view.setSections(sections)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BookmarksListPresenter: IBookmarksListPresenter {
|
||||
func viewDidLoad() {
|
||||
reload()
|
||||
view.setTitle(bookmarkGroup.title)
|
||||
view.setMoreItemTitle(L("placepage_more_button"))
|
||||
view.enableEditing(true)
|
||||
|
||||
let info = BookmarksListInfo(title: bookmarkGroup.title,
|
||||
description: bookmarkGroup.detailedAnnotation,
|
||||
hasDescription: bookmarkGroup.hasDescription,
|
||||
isHtmlDescription: bookmarkGroup.isHtmlDescription,
|
||||
imageUrl: bookmarkGroup.imageUrl,
|
||||
hasLogo: false)
|
||||
view.setInfo(info)
|
||||
}
|
||||
|
||||
func viewDidAppear() {
|
||||
reload()
|
||||
}
|
||||
|
||||
func activateSearch() {
|
||||
interactor.prepareForSearch()
|
||||
}
|
||||
|
||||
func deactivateSearch() {
|
||||
|
||||
}
|
||||
|
||||
func cancelSearch() {
|
||||
reload()
|
||||
}
|
||||
|
||||
func search(_ text: String) {
|
||||
interactor.search(text) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let bookmarks = self.mapBookmarks($0)
|
||||
self.view.setSections(bookmarks.isEmpty ? [] : [BookmarksSectionViewModel(title: L("bookmarks"),
|
||||
bookmarks: bookmarks)])
|
||||
}
|
||||
}
|
||||
|
||||
func more() {
|
||||
showMoreMenu()
|
||||
}
|
||||
|
||||
func sort() {
|
||||
showSortMenu()
|
||||
}
|
||||
|
||||
func deleteItem(in section: IBookmarksListSectionViewModel, at index: Int) {
|
||||
switch section {
|
||||
case let bookmarksSection as IBookmarksSectionViewModel:
|
||||
guard let bookmark = bookmarksSection.bookmarks[index] as? BookmarkViewModel else { fatalError() }
|
||||
interactor.deleteBookmark(bookmark.bookmarkId)
|
||||
reload()
|
||||
case let tracksSection as ITracksSectionViewModel:
|
||||
guard let track = tracksSection.tracks[index] as? TrackViewModel else { fatalError() }
|
||||
interactor.deleteTrack(track.trackId)
|
||||
reload()
|
||||
default:
|
||||
fatalError("Cannot delete item: unsupported section type: \(section.self)")
|
||||
}
|
||||
}
|
||||
|
||||
func moveItem(in section: IBookmarksListSectionViewModel, at index: Int) {
|
||||
let group = interactor.getBookmarkGroup()
|
||||
switch section {
|
||||
case let bookmarksSection as IBookmarksSectionViewModel:
|
||||
guard let bookmark = bookmarksSection.bookmarks[index] as? BookmarkViewModel else { fatalError() }
|
||||
editingItem = .bookmark(bookmark.bookmarkId)
|
||||
router.selectGroup(currentGroupName: group.title, currentGroupId: group.categoryId, delegate: self)
|
||||
case let tracksSection as ITracksSectionViewModel:
|
||||
guard let track = tracksSection.tracks[index] as? TrackViewModel else { fatalError() }
|
||||
editingItem = .track(track.trackId)
|
||||
router.selectGroup(currentGroupName: group.title, currentGroupId: group.categoryId, delegate: self)
|
||||
default:
|
||||
fatalError("Cannot move item: unsupported section type: \(section.self)")
|
||||
}
|
||||
}
|
||||
|
||||
func editItem(in section: IBookmarksListSectionViewModel, at index: Int) {
|
||||
switch section {
|
||||
case let bookmarksSection as IBookmarksSectionViewModel:
|
||||
guard let bookmarkId = (bookmarksSection.bookmarks[index] as? BookmarkViewModel)?.bookmarkId else { fatalError() }
|
||||
router.editBookmark(bookmarkId: bookmarkId) { [weak self] wasChanged in
|
||||
if wasChanged { self?.reload() }
|
||||
}
|
||||
case let tracksSection as ITracksSectionViewModel:
|
||||
guard let trackId = (tracksSection.tracks[index] as? TrackViewModel)?.trackId else { fatalError() }
|
||||
router.editTrack(trackId: trackId) { [weak self] wasChanged in
|
||||
if wasChanged { self?.reload() }
|
||||
}
|
||||
default:
|
||||
fatalError("Cannot edit item: unsupported section type: \(section.self)")
|
||||
}
|
||||
}
|
||||
|
||||
func selectItem(in section: IBookmarksListSectionViewModel, at index: Int) {
|
||||
switch section {
|
||||
case let bookmarksSection as IBookmarksSectionViewModel:
|
||||
let bookmark = bookmarksSection.bookmarks[index] as! BookmarkViewModel
|
||||
interactor.viewBookmarkOnMap(bookmark.bookmarkId)
|
||||
router.viewOnMap(bookmarkGroup)
|
||||
case let tracksSection as ITracksSectionViewModel:
|
||||
let track = tracksSection.tracks[index] as! TrackViewModel
|
||||
interactor.viewTrackOnMap(track.trackId)
|
||||
router.viewOnMap(bookmarkGroup)
|
||||
case let subgroupsSection as ISubgroupsSectionViewModel:
|
||||
let subgroup = subgroupsSection.subgroups[index] as! SubgroupViewModel
|
||||
router.showSubgroup(subgroup.groupId)
|
||||
if subgroup.type == .collection {
|
||||
} else if subgroup.type == .category {
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
default:
|
||||
fatalError("Wrong section type: \(section.self)")
|
||||
}
|
||||
}
|
||||
|
||||
func showDescription() {
|
||||
router.showDescription(bookmarkGroup)
|
||||
}
|
||||
|
||||
func checkItem(in section: IBookmarksListSectionViewModel, at index: Int, checked: Bool) {
|
||||
switch section {
|
||||
case let subgroupsSection as ISubgroupsSectionViewModel:
|
||||
let subgroup = subgroupsSection.subgroups[index] as! SubgroupViewModel
|
||||
interactor.setGroup(subgroup.groupId, visible: checked)
|
||||
reload()
|
||||
default:
|
||||
fatalError("Wrong section type: \(section.self)")
|
||||
}
|
||||
}
|
||||
|
||||
func toggleVisibility(in section: IBookmarksListSectionViewModel) {
|
||||
switch section {
|
||||
case let subgroupsSection as ISubgroupsSectionViewModel:
|
||||
let visible: Bool
|
||||
switch subgroupsSection.visibilityButtonState {
|
||||
case .hidden:
|
||||
fatalError("Unexpected visibility button state")
|
||||
case .hideAll:
|
||||
visible = false
|
||||
case .showAll:
|
||||
visible = true
|
||||
}
|
||||
subgroupsSection.subgroups.forEach {
|
||||
let subgroup = $0 as! SubgroupViewModel
|
||||
interactor.setGroup(subgroup.groupId, visible: visible)
|
||||
}
|
||||
reload()
|
||||
default:
|
||||
fatalError("Wrong section type: \(section.self)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BookmarksListPresenter: CategorySettingsViewControllerDelegate {
|
||||
func categorySettingsController(_ viewController: CategorySettingsViewController, didEndEditing categoryId: MWMMarkGroupID) {
|
||||
let info = BookmarksListInfo(title: bookmarkGroup.title,
|
||||
description: bookmarkGroup.detailedAnnotation,
|
||||
hasDescription: bookmarkGroup.hasDescription,
|
||||
isHtmlDescription: bookmarkGroup.isHtmlDescription,
|
||||
imageUrl: bookmarkGroup.imageUrl,
|
||||
hasLogo: false)
|
||||
view.setInfo(info)
|
||||
viewController.goBack()
|
||||
}
|
||||
|
||||
func categorySettingsController(_ viewController: CategorySettingsViewController, didDelete categoryId: MWMMarkGroupID) {
|
||||
if let delegate = delegate as? UIViewController {
|
||||
viewController.navigationController?.popToViewController(delegate, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BookmarksListPresenter: SelectBookmarkGroupViewControllerDelegate {
|
||||
func bookmarkGroupViewController(_ viewController: SelectBookmarkGroupViewController,
|
||||
didSelect groupTitle: String,
|
||||
groupId: MWMMarkGroupID) {
|
||||
|
||||
defer { viewController.dismiss(animated: true) }
|
||||
|
||||
guard groupId != bookmarkGroup.categoryId else { return }
|
||||
|
||||
switch editingItem {
|
||||
case .bookmark(let bookmarkId):
|
||||
interactor.moveBookmark(bookmarkId, toGroupId: groupId)
|
||||
case .track(let trackId):
|
||||
interactor.moveTrack(trackId, toGroupId: groupId)
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
|
||||
editingItem = nil
|
||||
|
||||
if bookmarkGroup.bookmarksCount > 0 || bookmarkGroup.trackCount > 0 {
|
||||
reload()
|
||||
} else {
|
||||
// if there are no bookmarks or tracks in current group no need to show this group
|
||||
// e.g. popping view controller 2 times
|
||||
if let rootNavigationController = viewController.presentingViewController as? UINavigationController {
|
||||
rootNavigationController.popViewController(animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension IBookmarksSectionViewModel {
|
||||
var numberOfItems: Int { bookmarks.count }
|
||||
var visibilityButtonState: BookmarksListVisibilityButtonState { .hidden }
|
||||
var canEdit: Bool { true }
|
||||
}
|
||||
|
||||
extension ITracksSectionViewModel {
|
||||
var numberOfItems: Int { tracks.count }
|
||||
var sectionTitle: String { L("tracks_title") }
|
||||
var visibilityButtonState: BookmarksListVisibilityButtonState { .hidden }
|
||||
var canEdit: Bool { true }
|
||||
}
|
||||
|
||||
extension ISubgroupsSectionViewModel {
|
||||
var numberOfItems: Int { subgroups.count }
|
||||
var visibilityButtonState: BookmarksListVisibilityButtonState {
|
||||
subgroups.reduce(false) { $0 ? $0 : $1.isVisible } ? .hideAll : .showAll
|
||||
}
|
||||
var canEdit: Bool { false }
|
||||
}
|
||||
|
||||
fileprivate struct BookmarkViewModel: IBookmarksListItemViewModel {
|
||||
let bookmarkId: MWMMarkID
|
||||
let name: String
|
||||
let subtitle: String
|
||||
var image: UIImage {
|
||||
bookmarkColor.image(bookmarkIconName)
|
||||
}
|
||||
var colorDidTapAction: (() -> Void)?
|
||||
|
||||
private let bookmarkColor: BookmarkColor
|
||||
private let bookmarkIconName: String
|
||||
|
||||
init(_ bookmark: Bookmark, formattedDistance: String?, colorDidTap: (() -> Void)?) {
|
||||
bookmarkId = bookmark.bookmarkId
|
||||
name = bookmark.bookmarkName
|
||||
bookmarkColor = bookmark.bookmarkColor
|
||||
bookmarkIconName = bookmark.bookmarkIconName
|
||||
subtitle = [formattedDistance, bookmark.bookmarkType].compactMap { $0 }.joined(separator: " • ")
|
||||
colorDidTapAction = colorDidTap
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct TrackViewModel: IBookmarksListItemViewModel {
|
||||
let trackId: MWMTrackID
|
||||
let name: String
|
||||
let subtitle: String
|
||||
var image: UIImage {
|
||||
circleImageForColor(trackColor, frameSize: 22)
|
||||
}
|
||||
var colorDidTapAction: (() -> Void)?
|
||||
|
||||
private let trackColor: UIColor
|
||||
|
||||
init(_ track: Track, formattedDistance: String, colorDidTap: (() -> Void)?) {
|
||||
trackId = track.trackId
|
||||
name = track.trackName
|
||||
subtitle = "\(L("length")) \(formattedDistance)"
|
||||
trackColor = track.trackColor
|
||||
colorDidTapAction = colorDidTap
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct SubgroupViewModel: ISubgroupViewModel {
|
||||
let groupId: MWMMarkGroupID
|
||||
let subgroupName: String
|
||||
let subtitle: String
|
||||
let isVisible: Bool
|
||||
let type: BookmarkGroupType
|
||||
|
||||
init(_ bookmarkGroup: BookmarkGroup) {
|
||||
groupId = bookmarkGroup.categoryId
|
||||
subgroupName = bookmarkGroup.title
|
||||
subtitle = bookmarkGroup.placesCountTitle()
|
||||
isVisible = bookmarkGroup.isVisible
|
||||
type = bookmarkGroup.type
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct BookmarksSectionViewModel: IBookmarksSectionViewModel {
|
||||
let sectionTitle: String
|
||||
let bookmarks: [IBookmarksListItemViewModel]
|
||||
|
||||
init(title: String, bookmarks: [IBookmarksListItemViewModel]) {
|
||||
sectionTitle = title
|
||||
self.bookmarks = bookmarks
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct TracksSectionViewModel: ITracksSectionViewModel {
|
||||
let tracks: [IBookmarksListItemViewModel]
|
||||
|
||||
init(tracks: [IBookmarksListItemViewModel]) {
|
||||
self.tracks = tracks
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct SubgroupsSectionViewModel: ISubgroupsSectionViewModel {
|
||||
let subgroups: [ISubgroupViewModel]
|
||||
let sectionTitle: String
|
||||
var type: BookmarkGroupType
|
||||
|
||||
init(title: String, subgroups: [ISubgroupViewModel], type: BookmarkGroupType) {
|
||||
sectionTitle = title
|
||||
self.type = type
|
||||
self.subgroups = subgroups
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct BookmarksListMenuItem: IBookmarksListMenuItem {
|
||||
let title: String
|
||||
let destructive: Bool
|
||||
let enabled: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(title: String, destructive: Bool = false, enabled: Bool = true, action: @escaping () -> Void) {
|
||||
self.title = title
|
||||
self.destructive = destructive
|
||||
self.enabled = enabled
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct BookmarksListInfo: IBookmarksListInfoViewModel {
|
||||
let title: String
|
||||
let description: String
|
||||
let hasDescription: Bool
|
||||
let isHtmlDescription: Bool
|
||||
let imageUrl: URL?
|
||||
let hasLogo: Bool
|
||||
|
||||
init(title: String, description: String, hasDescription: Bool, isHtmlDescription: Bool, imageUrl: URL? = nil, hasLogo: Bool = false) {
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.hasDescription = hasDescription
|
||||
self.isHtmlDescription = isHtmlDescription
|
||||
self.imageUrl = imageUrl
|
||||
self.hasLogo = hasLogo
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
final class BookmarksListRouter {
|
||||
private let mapViewController: MapViewController
|
||||
private weak var coordinator: BookmarksCoordinator?
|
||||
|
||||
init(_ mapViewController: MapViewController, bookmarksCoordinator: BookmarksCoordinator?) {
|
||||
self.mapViewController = mapViewController
|
||||
self.coordinator = bookmarksCoordinator
|
||||
}
|
||||
}
|
||||
|
||||
extension BookmarksListRouter: IBookmarksListRouter {
|
||||
func listSettings(_ bookmarkGroup: BookmarkGroup, delegate: CategorySettingsViewControllerDelegate?) {
|
||||
let listSettingsController = CategorySettingsViewController(bookmarkGroup: bookmarkGroup)
|
||||
listSettingsController.delegate = delegate
|
||||
mapViewController.navigationController?.pushViewController(listSettingsController, animated: true)
|
||||
}
|
||||
|
||||
func viewOnMap(_ bookmarkGroup: BookmarkGroup) {
|
||||
coordinator?.hide(categoryId: bookmarkGroup.categoryId)
|
||||
}
|
||||
|
||||
func showDescription(_ bookmarkGroup: BookmarkGroup) {
|
||||
let description = BookmarksListRouter.prepareHtmlDescription(bookmarkGroup)
|
||||
let descriptionViewController = WebViewController(html: description, baseUrl: nil, title: bookmarkGroup.title)!
|
||||
descriptionViewController.openInSafari = true
|
||||
mapViewController.navigationController?.pushViewController(descriptionViewController, animated: true)
|
||||
}
|
||||
|
||||
private static func prepareHtmlDescription(_ bookmarkGroup: BookmarkGroup) -> String {
|
||||
var description = bookmarkGroup.detailedAnnotation
|
||||
if bookmarkGroup.isHtmlDescription {
|
||||
if !description.contains("<body>") {
|
||||
description = "<body>" + description
|
||||
}
|
||||
} else {
|
||||
description = description.replacingOccurrences(of: "\n", with: "<br>")
|
||||
let header = """
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
line-height: 1.4;
|
||||
margin-right: 16px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
"""
|
||||
description = header + description
|
||||
}
|
||||
if !description.contains("</body>") {
|
||||
description += "</body>"
|
||||
}
|
||||
return description
|
||||
}
|
||||
|
||||
func showSubgroup(_ subgroupId: MWMMarkGroupID) {
|
||||
let bookmarksListViewController = BookmarksListBuilder.build(markGroupId: subgroupId,
|
||||
bookmarksCoordinator: coordinator)
|
||||
mapViewController.navigationController?.pushViewController(bookmarksListViewController, animated: true)
|
||||
}
|
||||
|
||||
func selectGroup(currentGroupName groupName: String,
|
||||
currentGroupId groupId: MWMMarkGroupID,
|
||||
delegate: SelectBookmarkGroupViewControllerDelegate?) {
|
||||
let groupViewController = SelectBookmarkGroupViewController(groupName: groupName, groupId: groupId)
|
||||
groupViewController.delegate = delegate
|
||||
let navigationController = UINavigationController(rootViewController: groupViewController)
|
||||
mapViewController.present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func editBookmark(bookmarkId: MWMMarkID, completion: @escaping (Bool) -> Void) {
|
||||
let editBookmarkController = UIStoryboard.instance(.main).instantiateViewController(withIdentifier: "MWMEditBookmarkController") as! EditBookmarkViewController
|
||||
editBookmarkController.configure(with: bookmarkId, editCompletion: completion)
|
||||
mapViewController.navigationController?.pushViewController(editBookmarkController, animated: true)
|
||||
}
|
||||
|
||||
func editTrack(trackId: MWMTrackID, completion: @escaping (Bool) -> Void) {
|
||||
let editTrackController = EditTrackViewController(trackId: trackId, editCompletion: completion)
|
||||
mapViewController.navigationController?.pushViewController(editTrackController, animated: true)
|
||||
}
|
||||
|
||||
func goBack() {
|
||||
coordinator?.goBack()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
final class BookmarksListViewController: MWMViewController {
|
||||
var presenter: IBookmarksListPresenter!
|
||||
|
||||
private var sections: [IBookmarksListSectionViewModel]?
|
||||
private let cellStrategy = BookmarksListCellStrategy()
|
||||
|
||||
private var canEdit = false
|
||||
|
||||
@IBOutlet private var tableView: UITableView!
|
||||
@IBOutlet private var toolBar: UIToolbar!
|
||||
@IBOutlet private var sortToolbarItem: UIBarButtonItem!
|
||||
@IBOutlet private var moreToolbarItem: UIBarButtonItem!
|
||||
private let searchController = UISearchController(searchResultsController: nil)
|
||||
|
||||
private lazy var infoViewController: BookmarksListInfoViewController = {
|
||||
let infoViewController = BookmarksListInfoViewController()
|
||||
infoViewController.delegate = self
|
||||
addChild(infoViewController)
|
||||
tableView.tableHeaderView = infoViewController.view
|
||||
infoViewController.didMove(toParent: self)
|
||||
return infoViewController
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let toolbarItemAttributes = [NSAttributedString.Key.font: UIFont.medium16(),
|
||||
NSAttributedString.Key.foregroundColor: UIColor.linkBlue()]
|
||||
|
||||
sortToolbarItem.setTitleTextAttributes(toolbarItemAttributes, for: .normal)
|
||||
moreToolbarItem.setTitleTextAttributes(toolbarItemAttributes, for: .normal)
|
||||
sortToolbarItem.title = L("sort")
|
||||
|
||||
extendedLayoutIncludesOpaqueBars = true
|
||||
searchController.searchBar.placeholder = L("search_in_the_list")
|
||||
searchController.obscuresBackgroundDuringPresentation = false
|
||||
searchController.hidesNavigationBarDuringPresentation = alternativeSizeClass(iPhone: true, iPad: false)
|
||||
searchController.searchBar.delegate = self
|
||||
searchController.searchBar.applyTheme()
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = false
|
||||
|
||||
cellStrategy.registerCells(tableView)
|
||||
cellStrategy.cellCheckHandler = { [weak self] (viewModel, index, checked) in
|
||||
self?.presenter.checkItem(in: viewModel, at: index, checked: checked)
|
||||
}
|
||||
cellStrategy.cellVisibilityHandler = { [weak self] viewModel in
|
||||
self?.presenter.toggleVisibility(in: viewModel)
|
||||
}
|
||||
presenter.viewDidLoad()
|
||||
MWMKeyboard.add(self);
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
presenter.viewDidAppear()
|
||||
}
|
||||
|
||||
deinit {
|
||||
MWMKeyboard.remove(self);
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
updateInfoSize()
|
||||
}
|
||||
|
||||
private func updateInfoSize() {
|
||||
guard let infoView = infoViewController.view else { return }
|
||||
let infoViewSize = infoView.systemLayoutSizeFitting(CGSize(width: view.width, height: 0),
|
||||
withHorizontalFittingPriority: .required,
|
||||
verticalFittingPriority: .fittingSizeLevel)
|
||||
infoView.size = infoViewSize
|
||||
tableView.tableHeaderView = infoView
|
||||
}
|
||||
|
||||
@IBAction private func onSortItem(_ sender: UIBarButtonItem) {
|
||||
presenter.sort()
|
||||
}
|
||||
|
||||
@IBAction private func onMoreItem(_ sender: UIBarButtonItem) {
|
||||
presenter.more()
|
||||
}
|
||||
|
||||
override func setEditing(_ editing: Bool, animated: Bool) {
|
||||
super.setEditing(editing, animated: animated)
|
||||
tableView.setEditing(editing, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
extension BookmarksListViewController: UITableViewDataSource {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
sections?.count ?? 0
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
guard let section = sections?[section] else { fatalError() }
|
||||
return section.numberOfItems
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let section = sections?[indexPath.section] else { fatalError() }
|
||||
return cellStrategy.tableCell(tableView, for: section, at: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
extension BookmarksListViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
60
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
guard let section = sections?[section] else { fatalError() }
|
||||
return cellStrategy.headerView(tableView, for: section)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
return indexPath
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard let section = sections?[indexPath.section] else { fatalError() }
|
||||
presenter.selectItem(in: section, at: indexPath.row)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
guard let section = sections?[indexPath.section] else { fatalError() }
|
||||
return canEdit && section.canEdit
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
|
||||
isEditing = true
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) {
|
||||
isEditing = false
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView,
|
||||
leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let moveAction = UIContextualAction(style: .normal, title: L("move")) { [weak self] (_, _, completion) in
|
||||
guard let section = self?.sections?[indexPath.section] else { fatalError() }
|
||||
self?.presenter.moveItem(in: section, at: indexPath.row)
|
||||
completion(true)
|
||||
}
|
||||
return UISwipeActionsConfiguration(actions: [moveAction])
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView,
|
||||
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let deleteAction = UIContextualAction(style: .destructive, title: L("delete")) { [weak self] (_, _, completion) in
|
||||
guard let section = self?.sections?[indexPath.section] else { fatalError() }
|
||||
self?.presenter.deleteItem(in: section, at: indexPath.row)
|
||||
completion(true)
|
||||
}
|
||||
let editAction = UIContextualAction(style: .normal, title: L("edit")) { [weak self] (_, _, completion) in
|
||||
guard let section = self?.sections?[indexPath.section] else { fatalError() }
|
||||
self?.presenter.editItem(in: section, at: indexPath.row)
|
||||
completion(true)
|
||||
}
|
||||
return UISwipeActionsConfiguration(actions: [deleteAction, editAction])
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
|
||||
guard let section = sections?[indexPath.section] else { fatalError() }
|
||||
presenter.editItem(in: section, at: indexPath.row)
|
||||
}
|
||||
}
|
||||
|
||||
extension BookmarksListViewController: UISearchBarDelegate {
|
||||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
searchBar.setShowsCancelButton(true, animated: true)
|
||||
presenter.activateSearch()
|
||||
}
|
||||
|
||||
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
||||
searchBar.setShowsCancelButton(false, animated: true)
|
||||
presenter.deactivateSearch()
|
||||
}
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.text = nil
|
||||
searchBar.resignFirstResponder()
|
||||
presenter.cancelSearch()
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
guard !searchText.isEmpty else {
|
||||
presenter.cancelSearch()
|
||||
return
|
||||
}
|
||||
|
||||
presenter.search(searchText)
|
||||
}
|
||||
}
|
||||
|
||||
extension BookmarksListViewController: IBookmarksListView {
|
||||
func setTitle(_ title: String) {
|
||||
self.title = title
|
||||
}
|
||||
|
||||
func setInfo(_ info: IBookmarksListInfoViewModel) {
|
||||
infoViewController.info = info
|
||||
updateInfoSize()
|
||||
}
|
||||
|
||||
func setSections(_ sections: [IBookmarksListSectionViewModel]) {
|
||||
self.sections = sections
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
func setMoreItemTitle(_ itemTitle: String) {
|
||||
moreToolbarItem.title = itemTitle
|
||||
}
|
||||
|
||||
func showMenu(_ items: [IBookmarksListMenuItem], from source: BookmarkToolbarButtonSource) {
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
items.forEach { item in
|
||||
let action = UIAlertAction(title: item.title, style: item.destructive ? .destructive : .default) { _ in
|
||||
item.action()
|
||||
}
|
||||
action.isEnabled = item.enabled
|
||||
actionSheet.addAction(action)
|
||||
}
|
||||
actionSheet.addAction(UIAlertAction(title: L("cancel"), style: .cancel, handler: nil))
|
||||
let barButtonItem = switch source {
|
||||
case .sort: sortToolbarItem
|
||||
case .more: moreToolbarItem
|
||||
}
|
||||
actionSheet.popoverPresentationController?.barButtonItem = barButtonItem
|
||||
present(actionSheet, animated: true)
|
||||
}
|
||||
|
||||
func showColorPicker(with pickerType: ColorPickerType, _ completionHandler: ((UIColor) -> Void)?) {
|
||||
ColorPicker.shared.present(from: self, pickerType: pickerType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func enableEditing(_ enable: Bool) {
|
||||
canEdit = enable
|
||||
navigationItem.rightBarButtonItem = enable ? editButtonItem : nil
|
||||
}
|
||||
|
||||
func share(_ url: URL, completion: @escaping () -> Void) {
|
||||
let shareController = ActivityViewController.share(for: url,
|
||||
message: L("share_bookmarks_email_body")) { (_, _, _, _) in
|
||||
completion()
|
||||
}
|
||||
shareController.present(inParentViewController: self, anchorView: self.toolBar)
|
||||
}
|
||||
|
||||
func showError(title: String, message: String) {
|
||||
MWMAlertViewController.activeAlert().presentInfoAlert(title, text: message)
|
||||
}
|
||||
}
|
||||
|
||||
extension BookmarksListViewController: BookmarksListInfoViewControllerDelegate {
|
||||
func didPressDescription() {
|
||||
presenter.showDescription()
|
||||
}
|
||||
|
||||
func didUpdateContent() {
|
||||
updateInfoSize()
|
||||
}
|
||||
}
|
||||
|
||||
extension BookmarksListViewController: MWMKeyboardObserver {
|
||||
func onKeyboardAnimation() {
|
||||
let keyboardHeight = MWMKeyboard.keyboardHeight();
|
||||
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="BookmarksListViewController" customModule="CoMaps" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="moreToolbarItem" destination="Hhy-7w-Mz0" id="0bI-d2-WuP"/>
|
||||
<outlet property="sortToolbarItem" destination="BWR-ft-be3" id="iiS-BA-nqF"/>
|
||||
<outlet property="tableView" destination="fva-qQ-WqU" id="XoT-Z8-nGb"/>
|
||||
<outlet property="toolBar" destination="cD8-kh-Gmp" id="1M2-EB-fbH"/>
|
||||
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="60" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="fva-qQ-WqU">
|
||||
<rect key="frame" x="0.0" y="56" width="375" height="567"/>
|
||||
<color key="backgroundColor" systemColor="groupTableViewBackgroundColor"/>
|
||||
<inset key="separatorInset" minX="56" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="TableView:PressBackground"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="-1" id="1RG-NX-TL8"/>
|
||||
<outlet property="delegate" destination="-1" id="Djq-Nz-win"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cD8-kh-Gmp">
|
||||
<rect key="frame" x="0.0" y="623" width="375" height="44"/>
|
||||
<items>
|
||||
<barButtonItem width="8" style="plain" systemItem="fixedSpace" id="E0V-Le-AEM"/>
|
||||
<barButtonItem title="Sort" style="plain" id="BWR-ft-be3">
|
||||
<connections>
|
||||
<action selector="onSortItem:" destination="-1" id="bcl-jw-IY1"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem style="plain" systemItem="flexibleSpace" id="z0r-ms-Z5o"/>
|
||||
<barButtonItem title="More..." style="plain" id="Hhy-7w-Mz0">
|
||||
<connections>
|
||||
<action selector="onMoreItem:" destination="-1" id="P0K-AE-UgC"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem width="8" style="plain" systemItem="fixedSpace" id="9Q2-6r-Qjv"/>
|
||||
</items>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="Background"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</toolbar>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" secondItem="cD8-kh-Gmp" secondAttribute="bottom" id="FgV-RT-cCW"/>
|
||||
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="cD8-kh-Gmp" secondAttribute="trailing" id="LZs-Au-R4I"/>
|
||||
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="fva-qQ-WqU" secondAttribute="trailing" id="OsX-Pn-Js4"/>
|
||||
<constraint firstItem="fva-qQ-WqU" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="d61-I4-Ku3"/>
|
||||
<constraint firstItem="cD8-kh-Gmp" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="jVJ-nY-UBF"/>
|
||||
<constraint firstItem="cD8-kh-Gmp" firstAttribute="top" secondItem="fva-qQ-WqU" secondAttribute="bottom" id="lQL-J0-O69"/>
|
||||
<constraint firstItem="fva-qQ-WqU" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="yZs-bO-F39"/>
|
||||
</constraints>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="PressBackground"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<point key="canvasLocation" x="137.68115942028987" y="95.758928571428569"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="groupTableViewBackgroundColor">
|
||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
final class BookmarksListCell: UITableViewCell {
|
||||
|
||||
private static let extendedImageViewTappableMargin: CGFloat = -15
|
||||
|
||||
private var trackColorDidTapAction: (() -> Void)?
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
setupCell()
|
||||
}
|
||||
|
||||
private func setupCell() {
|
||||
accessoryType = .detailButton
|
||||
if let imageView {
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(colorDidTapAction(_:)))
|
||||
imageView.addGestureRecognizer(tapGesture)
|
||||
imageView.isUserInteractionEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
func config(_ bookmark: IBookmarksListItemViewModel) {
|
||||
imageView?.image = bookmark.image
|
||||
textLabel?.text = bookmark.name
|
||||
detailTextLabel?.text = bookmark.subtitle
|
||||
trackColorDidTapAction = bookmark.colorDidTapAction
|
||||
}
|
||||
|
||||
@objc private func colorDidTapAction(_ sender: UITapGestureRecognizer) {
|
||||
trackColorDidTapAction?()
|
||||
}
|
||||
|
||||
// Extends the imageView tappable area.
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let imageView, imageView.convert(imageView.bounds, to: self).insetBy(dx: Self.extendedImageViewTappableMargin, dy: Self.extendedImageViewTappableMargin).contains(point) {
|
||||
return imageView
|
||||
} else {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" textLabel="QgU-jK-sWK" detailTextLabel="zC9-YD-CfS" rowHeight="64" style="IBUITableViewCellStyleSubtitle" id="KGk-i7-Jjw" customClass="BookmarksListCell" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="64"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="64"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="QgU-jK-sWK">
|
||||
<rect key="frame" x="16" y="16" width="25" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular16:blackPrimaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="zC9-YD-CfS">
|
||||
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="0.0"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular14:blackSecondaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<point key="canvasLocation" x="137.68115942028987" y="104.12946428571428"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
final class BookmarksListCellStrategy {
|
||||
private enum CellId {
|
||||
static let listItem = "BookmarksListCell"
|
||||
static let subgroup = "SubgroupCell"
|
||||
static let sectionHeader = "SectionHeader"
|
||||
}
|
||||
|
||||
typealias CheckHandlerClosure = (IBookmarksListSectionViewModel, Int, Bool) -> Void
|
||||
var cellCheckHandler: CheckHandlerClosure?
|
||||
|
||||
typealias VisibilityHandlerClosure = (IBookmarksListSectionViewModel) -> Void
|
||||
var cellVisibilityHandler: VisibilityHandlerClosure?
|
||||
|
||||
func registerCells(_ tableView: UITableView) {
|
||||
tableView.register(UINib(nibName: "BookmarksListCell", bundle: nil), forCellReuseIdentifier: CellId.listItem)
|
||||
tableView.register(UINib(nibName: "SubgroupCell", bundle: nil), forCellReuseIdentifier: CellId.subgroup)
|
||||
tableView.register(UINib(nibName: "BookmarksListSectionHeader", bundle: nil),
|
||||
forHeaderFooterViewReuseIdentifier: CellId.sectionHeader)
|
||||
}
|
||||
|
||||
func tableCell(_ tableView: UITableView,
|
||||
for viewModel: IBookmarksListSectionViewModel,
|
||||
at indexPath: IndexPath) -> UITableViewCell {
|
||||
switch viewModel {
|
||||
case let bookmarksSection as IBookmarksSectionViewModel:
|
||||
let bookmark = bookmarksSection.bookmarks[indexPath.row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: CellId.listItem, for: indexPath) as! BookmarksListCell
|
||||
cell.config(bookmark)
|
||||
return cell
|
||||
case let tracksSection as ITracksSectionViewModel:
|
||||
let track = tracksSection.tracks[indexPath.row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: CellId.listItem, for: indexPath) as! BookmarksListCell
|
||||
cell.config(track)
|
||||
return cell
|
||||
case let subgroupsSection as ISubgroupsSectionViewModel:
|
||||
let subgroup = subgroupsSection.subgroups[indexPath.row]
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: CellId.subgroup, for: indexPath) as! SubgroupCell
|
||||
cell.config(subgroup)
|
||||
cell.checkHandler = { [weak self] checked in
|
||||
self?.cellCheckHandler?(viewModel, indexPath.row, checked)
|
||||
}
|
||||
return cell
|
||||
default:
|
||||
fatalError("Unexpected item")
|
||||
}
|
||||
}
|
||||
|
||||
func headerView(_ tableView: UITableView,
|
||||
for viewModel: IBookmarksListSectionViewModel) -> UITableViewHeaderFooterView {
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: CellId.sectionHeader)
|
||||
as! BookmarksListSectionHeader
|
||||
headerView.config(viewModel)
|
||||
headerView.visibilityHandler = { [weak self] in
|
||||
self?.cellVisibilityHandler?(viewModel)
|
||||
}
|
||||
return headerView
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
final class BookmarksListSectionHeader: UITableViewHeaderFooterView {
|
||||
@IBOutlet private var titleLabel: UILabel!
|
||||
@IBOutlet private var visibilityButton: UIButton!
|
||||
|
||||
typealias VisibilityHandlerClosure = () -> Void
|
||||
var visibilityHandler: VisibilityHandlerClosure?
|
||||
|
||||
@IBAction private func onVisibilityButton(_ sender: UIButton) {
|
||||
visibilityHandler?()
|
||||
}
|
||||
|
||||
func config(_ section: IBookmarksListSectionViewModel) {
|
||||
titleLabel.text = section.sectionTitle
|
||||
switch section.visibilityButtonState {
|
||||
case .hidden:
|
||||
visibilityButton.isHidden = true
|
||||
case .hideAll:
|
||||
visibilityButton.isHidden = false
|
||||
visibilityButton.setTitle(L("bookmark_lists_hide_all"), for: .normal)
|
||||
case .showAll:
|
||||
visibilityButton.isHidden = false
|
||||
visibilityButton.setTitle(L("bookmark_lists_show_all"), for: .normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="iN0-l3-epB" customClass="BookmarksListSectionHeader" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="xCm-Dh-8mT">
|
||||
<rect key="frame" x="16" y="12" width="343" height="48"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Bookmarks" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="yNv-Yr-Hkr">
|
||||
<rect key="frame" x="0.0" y="0.0" width="287" height="48"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="20"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="bold20:blackPrimaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" horizontalCompressionResistancePriority="751" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="zdf-Q0-3xO">
|
||||
<rect key="frame" x="287" y="0.0" width="56" height="48"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<state key="normal" title="Hide All"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="linkBlueText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="onVisibilityButton:" destination="iN0-l3-epB" eventType="touchUpInside" id="w2u-1a-M9u"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="xCm-Dh-8mT" secondAttribute="bottom" id="3MG-Ep-gMo"/>
|
||||
<constraint firstItem="xCm-Dh-8mT" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="16" id="9XD-d8-7vk"/>
|
||||
<constraint firstAttribute="trailing" secondItem="xCm-Dh-8mT" secondAttribute="trailing" constant="16" id="OGF-TS-kyi"/>
|
||||
<constraint firstItem="xCm-Dh-8mT" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="12" id="OLp-AI-xo9"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="titleLabel" destination="yNv-Yr-Hkr" id="2D1-wr-wFj"/>
|
||||
<outlet property="visibilityButton" destination="zdf-Q0-3xO" id="Z64-hc-gnc"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="138.40000000000001" y="201.94902548725639"/>
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
||||
18
iphone/Maps/Bookmarks/BookmarksList/Cells/SubgroupCell.swift
Normal file
18
iphone/Maps/Bookmarks/BookmarksList/Cells/SubgroupCell.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
final class SubgroupCell: UITableViewCell {
|
||||
@IBOutlet private var subgroupTitleLabel: UILabel!
|
||||
@IBOutlet private var subgroupSubtitleLabel: UILabel!
|
||||
@IBOutlet private var subgroupVisibleMark: Checkmark!
|
||||
|
||||
typealias CheckHandlerClosure = (Bool) -> Void
|
||||
var checkHandler: CheckHandlerClosure?
|
||||
|
||||
func config(_ subgroup: ISubgroupViewModel) {
|
||||
subgroupTitleLabel.text = subgroup.subgroupName
|
||||
subgroupSubtitleLabel.text = subgroup.subtitle
|
||||
subgroupVisibleMark.isChecked = subgroup.isVisible
|
||||
}
|
||||
|
||||
@IBAction private func onCheck(_ sender: Checkmark) {
|
||||
checkHandler?(sender.isChecked)
|
||||
}
|
||||
}
|
||||
87
iphone/Maps/Bookmarks/BookmarksList/Cells/SubgroupCell.xib
Normal file
87
iphone/Maps/Bookmarks/BookmarksList/Cells/SubgroupCell.xib
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="64" id="KGk-i7-Jjw" customClass="SubgroupCell" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="64"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="64"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" horizontalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="mAZ-hb-J9T" customClass="Checkmark" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="56" height="64"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="56" id="aar-GM-DaJ"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="image" keyPath="offImage" value="ic_eye_off"/>
|
||||
<userDefinedRuntimeAttribute type="image" keyPath="onImage" value="ic_eye_on"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="onCheck:" destination="KGk-i7-Jjw" eventType="valueChanged" id="xp2-kL-Hgu"/>
|
||||
</connections>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="L2V-JY-eeP">
|
||||
<rect key="frame" x="56" y="11" width="224" height="20"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular16:blackPrimaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="mcK-1H-ZBS">
|
||||
<rect key="frame" x="56" y="35" width="224" height="16.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular14:blackSecondaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_disclosure" translatesAutoresizingMaskIntoConstraints="NO" id="UOn-k5-sEA">
|
||||
<rect key="frame" x="288" y="20" width="24" height="24"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MWMGray"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="L2V-JY-eeP" firstAttribute="leading" secondItem="mAZ-hb-J9T" secondAttribute="trailing" id="3fE-vM-7lQ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="mcK-1H-ZBS" secondAttribute="bottom" constant="12.5" id="Ca4-dk-CyT"/>
|
||||
<constraint firstAttribute="bottom" secondItem="mAZ-hb-J9T" secondAttribute="bottom" id="EEL-tz-cXw"/>
|
||||
<constraint firstItem="UOn-k5-sEA" firstAttribute="leading" secondItem="mcK-1H-ZBS" secondAttribute="trailing" constant="8" id="GMX-F4-EWk"/>
|
||||
<constraint firstAttribute="trailing" secondItem="UOn-k5-sEA" secondAttribute="trailing" constant="8" id="Pdg-cN-1Py"/>
|
||||
<constraint firstItem="mAZ-hb-J9T" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="SzN-rJ-pMf"/>
|
||||
<constraint firstItem="L2V-JY-eeP" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="11" id="ayo-1v-WC2"/>
|
||||
<constraint firstItem="mcK-1H-ZBS" firstAttribute="leading" secondItem="mAZ-hb-J9T" secondAttribute="trailing" id="bsr-4x-bfr"/>
|
||||
<constraint firstItem="mAZ-hb-J9T" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="gws-UG-2oB"/>
|
||||
<constraint firstItem="UOn-k5-sEA" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="hTX-sY-kSJ"/>
|
||||
<constraint firstItem="UOn-k5-sEA" firstAttribute="leading" secondItem="L2V-JY-eeP" secondAttribute="trailing" constant="8" id="pua-Zn-zVM"/>
|
||||
<constraint firstItem="mcK-1H-ZBS" firstAttribute="top" secondItem="L2V-JY-eeP" secondAttribute="bottom" constant="4" id="ycS-hQ-Y91"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="subgroupSubtitleLabel" destination="mcK-1H-ZBS" id="x96-KI-R0D"/>
|
||||
<outlet property="subgroupTitleLabel" destination="L2V-JY-eeP" id="ZUX-Yy-zHY"/>
|
||||
<outlet property="subgroupVisibleMark" destination="mAZ-hb-J9T" id="zrV-cz-C1i"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="127.53623188405798" y="60.267857142857139"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="ic_disclosure" width="24" height="24"/>
|
||||
<image name="ic_eye_off" width="24" height="24"/>
|
||||
<image name="ic_eye_on" width="24" height="24"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
final class BMCActionsCell: MWMTableViewCell {
|
||||
@IBOutlet private weak var actionImage: UIImageView!
|
||||
|
||||
@IBOutlet private weak var actionTitle: UILabel!
|
||||
|
||||
private var model: BMCAction! {
|
||||
didSet {
|
||||
actionImage.image = model.image
|
||||
actionTitle.text = model.title
|
||||
}
|
||||
}
|
||||
|
||||
func config(model: BMCAction) -> UITableViewCell {
|
||||
self.model = model
|
||||
return self
|
||||
}
|
||||
}
|
||||
71
iphone/Maps/Bookmarks/Categories/Actions/BMCActionsCell.xib
Normal file
71
iphone/Maps/Bookmarks/Categories/Actions/BMCActionsCell.xib
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="BMCActionsCell" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YDi-5J-vFD">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic24PxAddCopy" translatesAutoresizingMaskIntoConstraints="NO" id="paw-km-zXg">
|
||||
<rect key="frame" x="16" y="10" width="24" height="24"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="24" id="S7u-WM-dEL"/>
|
||||
<constraint firstAttribute="width" constant="24" id="WgB-k6-NoZ"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MWMBlue"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2jJ-Pu-pjy">
|
||||
<rect key="frame" x="56" y="11.5" width="248" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular16:blackPrimaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="paw-km-zXg" firstAttribute="centerY" secondItem="YDi-5J-vFD" secondAttribute="centerY" id="Oe0-2d-YX4"/>
|
||||
<constraint firstAttribute="trailing" secondItem="2jJ-Pu-pjy" secondAttribute="trailing" constant="16" id="ZGg-bD-dnT"/>
|
||||
<constraint firstItem="2jJ-Pu-pjy" firstAttribute="centerY" secondItem="YDi-5J-vFD" secondAttribute="centerY" id="h3L-zB-b66"/>
|
||||
<constraint firstItem="paw-km-zXg" firstAttribute="leading" secondItem="YDi-5J-vFD" secondAttribute="leading" constant="16" id="hTc-Pj-9Kf"/>
|
||||
<constraint firstItem="2jJ-Pu-pjy" firstAttribute="leading" secondItem="paw-km-zXg" secondAttribute="trailing" constant="16" id="maj-Zr-I2l"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="YDi-5J-vFD" secondAttribute="trailing" id="0yF-ib-wdg"/>
|
||||
<constraint firstItem="YDi-5J-vFD" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="7B2-Af-3Ko"/>
|
||||
<constraint firstAttribute="bottom" secondItem="YDi-5J-vFD" secondAttribute="bottom" id="HQk-FG-Enc"/>
|
||||
<constraint firstItem="YDi-5J-vFD" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="oCB-5w-Deh"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="actionImage" destination="paw-km-zXg" id="CfG-Wg-jon"/>
|
||||
<outlet property="actionTitle" destination="2jJ-Pu-pjy" id="YFu-jA-Abl"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="139" y="155"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="ic24PxAddCopy" width="24" height="24"/>
|
||||
</resources>
|
||||
</document>
|
||||
67
iphone/Maps/Bookmarks/Categories/BMCActionsCreateCell.xib
Normal file
67
iphone/Maps/Bookmarks/Categories/BMCActionsCreateCell.xib
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="BMCActionsCreateCell" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YDi-5J-vFD">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic24PxAddCopy" translatesAutoresizingMaskIntoConstraints="NO" id="paw-km-zXg">
|
||||
<rect key="frame" x="16" y="10" width="24" height="24"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="24" id="S7u-WM-dEL"/>
|
||||
<constraint firstAttribute="width" constant="24" id="WgB-k6-NoZ"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2jJ-Pu-pjy">
|
||||
<rect key="frame" x="56" y="11.5" width="248" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="paw-km-zXg" firstAttribute="centerY" secondItem="YDi-5J-vFD" secondAttribute="centerY" id="Oe0-2d-YX4"/>
|
||||
<constraint firstAttribute="trailing" secondItem="2jJ-Pu-pjy" secondAttribute="trailing" constant="16" id="ZGg-bD-dnT"/>
|
||||
<constraint firstItem="2jJ-Pu-pjy" firstAttribute="centerY" secondItem="YDi-5J-vFD" secondAttribute="centerY" id="h3L-zB-b66"/>
|
||||
<constraint firstItem="paw-km-zXg" firstAttribute="leading" secondItem="YDi-5J-vFD" secondAttribute="leading" constant="16" id="hTc-Pj-9Kf"/>
|
||||
<constraint firstItem="2jJ-Pu-pjy" firstAttribute="leading" secondItem="paw-km-zXg" secondAttribute="trailing" constant="16" id="maj-Zr-I2l"/>
|
||||
<constraint firstAttribute="height" constant="44" id="rhS-LZ-kol"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="YDi-5J-vFD" secondAttribute="trailing" id="0yF-ib-wdg"/>
|
||||
<constraint firstItem="YDi-5J-vFD" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="7B2-Af-3Ko"/>
|
||||
<constraint firstAttribute="bottom" secondItem="YDi-5J-vFD" secondAttribute="bottom" id="HQk-FG-Enc"/>
|
||||
<constraint firstItem="YDi-5J-vFD" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="oCB-5w-Deh"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="actionImage" destination="paw-km-zXg" id="CfG-Wg-jon"/>
|
||||
<outlet property="actionTitle" destination="2jJ-Pu-pjy" id="YFu-jA-Abl"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="ic24PxAddCopy" width="24" height="24"/>
|
||||
</resources>
|
||||
</document>
|
||||
47
iphone/Maps/Bookmarks/Categories/BMCModels.swift
Normal file
47
iphone/Maps/Bookmarks/Categories/BMCModels.swift
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
enum BMCSection {
|
||||
case categories
|
||||
case actions
|
||||
case recentlyDeleted
|
||||
case notifications
|
||||
}
|
||||
|
||||
protocol BMCModel {}
|
||||
|
||||
enum BMCAction: BMCModel {
|
||||
case create
|
||||
case exportAll
|
||||
case `import`
|
||||
case recentlyDeleted(Int)
|
||||
}
|
||||
|
||||
extension BMCAction {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .create:
|
||||
return L("bookmarks_create_new_group")
|
||||
case .exportAll:
|
||||
return L("bookmarks_export")
|
||||
case .import:
|
||||
return L("bookmarks_import")
|
||||
case .recentlyDeleted(let count):
|
||||
return L("bookmarks_recently_deleted") + " (\(count))"
|
||||
}
|
||||
}
|
||||
|
||||
var image: UIImage {
|
||||
switch self {
|
||||
case .create:
|
||||
return UIImage(named: "ic24PxAddCopy")!
|
||||
case .exportAll:
|
||||
return UIImage(named: "ic24PxShare")!
|
||||
case .import:
|
||||
return UIImage(named: "ic24PxImport")!
|
||||
case .recentlyDeleted:
|
||||
return UIImage(named: "ic_route_manager_trash_open")!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum BMCNotification: BMCModel {
|
||||
case load
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
final class BMCEmtyDescriptionCell: UITableViewCell {
|
||||
}
|
||||
333
iphone/Maps/Bookmarks/Categories/BMCView/BMCViewController.swift
Normal file
333
iphone/Maps/Bookmarks/Categories/BMCView/BMCViewController.swift
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
final class BMCViewController: MWMViewController {
|
||||
private var viewModel: BMCDefaultViewModel! {
|
||||
didSet {
|
||||
viewModel.view = self
|
||||
tableView.dataSource = self
|
||||
}
|
||||
}
|
||||
|
||||
private weak var coordinator: BookmarksCoordinator?
|
||||
|
||||
@IBOutlet private weak var tableView: UITableView! {
|
||||
didSet {
|
||||
let cells = [
|
||||
BMCCategoryCell.self,
|
||||
BMCActionsCell.self,
|
||||
BMCNotificationsCell.self,
|
||||
]
|
||||
tableView.registerNibs(cells)
|
||||
tableView.registerNibForHeaderFooterView(BMCCategoriesHeader.self)
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet private var actionsHeader: UIView!
|
||||
@IBOutlet private var notificationsHeader: BMCNotificationsHeader!
|
||||
|
||||
init(coordinator: BookmarksCoordinator?) {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
self.coordinator = coordinator
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.setStyle(.pressBackground)
|
||||
viewModel = BMCDefaultViewModel()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
viewModel.addToObserverList()
|
||||
viewModel.reloadData()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
viewModel.removeFromObserverList()
|
||||
}
|
||||
|
||||
private func createNewCategory() {
|
||||
alertController.presentCreateBookmarkCategoryAlert(withMaxCharacterNum: viewModel.maxCategoryNameLength,
|
||||
minCharacterNum: viewModel.minCategoryNameLength)
|
||||
{ [weak viewModel] (name: String!) -> Bool in
|
||||
guard let model = viewModel else { return false }
|
||||
if model.checkCategory(name: name) {
|
||||
model.addCategory(name: name)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func shareCategoryFile(at index: Int, fileType: KmlFileType, anchor: UIView) {
|
||||
UIApplication.shared.showLoadingOverlay()
|
||||
viewModel.shareCategoryFile(at: index, fileType: fileType, handler: sharingResultHandler(anchorView: anchor))
|
||||
}
|
||||
|
||||
private func shareAllCategories(anchor: UIView?) {
|
||||
UIApplication.shared.showLoadingOverlay()
|
||||
viewModel.shareAllCategories(handler: sharingResultHandler(anchorView: anchor))
|
||||
}
|
||||
|
||||
private func sharingResultHandler(anchorView: UIView?) -> SharingResultCompletionHandler {
|
||||
{ [weak self] status, url in
|
||||
UIApplication.shared.hideLoadingOverlay {
|
||||
guard let self else { return }
|
||||
switch status {
|
||||
case .success:
|
||||
let shareController = ActivityViewController.share(for: url, message: L("share_bookmarks_email_body"))
|
||||
{ [weak self] _, _, _, _ in
|
||||
self?.viewModel?.finishShareCategory()
|
||||
}
|
||||
shareController.present(inParentViewController: self, anchorView: anchorView)
|
||||
case .emptyCategory:
|
||||
MWMAlertViewController.activeAlert().presentInfoAlert(L("bookmarks_error_title_share_empty"),
|
||||
text: L("bookmarks_error_message_share_empty"))
|
||||
case .fileError, .archiveError:
|
||||
MWMAlertViewController.activeAlert().presentInfoAlert(L("dialog_routing_system_error"),
|
||||
text: L("bookmarks_error_message_share_general"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showImportDialog() {
|
||||
DocumentPicker.shared.present(from: self) { [viewModel] urls in
|
||||
viewModel?.importCategories(from: urls)
|
||||
}
|
||||
}
|
||||
|
||||
private func openCategorySettings(category: BookmarkGroup) {
|
||||
let settingsController = CategorySettingsViewController(bookmarkGroup: BookmarksManager.shared().category(withId: category.categoryId))
|
||||
settingsController.delegate = self
|
||||
|
||||
MapViewController.shared()?.navigationController?.pushViewController(settingsController, animated: true)
|
||||
}
|
||||
|
||||
private func openCategory(category: BookmarkGroup) {
|
||||
let bmViewController = BookmarksListBuilder.build(markGroupId: category.categoryId,
|
||||
bookmarksCoordinator: coordinator,
|
||||
delegate: self)
|
||||
MapViewController.shared()?.navigationController?.pushViewController(bmViewController, animated: true)
|
||||
}
|
||||
|
||||
private func setCategoryVisible(_ visible: Bool, at index: Int) {
|
||||
let category = viewModel.category(at: index)
|
||||
BookmarksManager.shared().setCategory(category.categoryId, isVisible: visible)
|
||||
if let categoriesHeader = tableView.headerView(forSection: viewModel.sectionIndex(section: .categories)) as? BMCCategoriesHeader {
|
||||
categoriesHeader.isShowAll = viewModel.areAllCategoriesHidden()
|
||||
}
|
||||
}
|
||||
|
||||
private func editCategory(at index: Int, anchor: UIView) {
|
||||
let category = viewModel.category(at: index)
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
if let ppc = actionSheet.popoverPresentationController {
|
||||
ppc.sourceView = anchor
|
||||
ppc.sourceRect = anchor.bounds
|
||||
}
|
||||
|
||||
let settings = L("edit")
|
||||
actionSheet.addAction(UIAlertAction(title: settings, style: .default, handler: { _ in
|
||||
self.openCategorySettings(category: category)
|
||||
}))
|
||||
let showHide = L(category.isVisible ? "hide_from_map" : "zoom_to_country")
|
||||
actionSheet.addAction(UIAlertAction(title: showHide, style: .default, handler: { _ in
|
||||
self.setCategoryVisible(!category.isVisible, at: index)
|
||||
let sectionIndex = self.viewModel.sectionIndex(section: .categories)
|
||||
self.tableView.reloadRows(at: [IndexPath(row: index, section: sectionIndex)], with: .none)
|
||||
}))
|
||||
actionSheet.addAction(UIAlertAction(title: L("export_file"), style: .default, handler: { _ in
|
||||
self.shareCategoryFile(at: index, fileType: .text, anchor: anchor)
|
||||
}))
|
||||
actionSheet.addAction(UIAlertAction(title: L("export_file_gpx"), style: .default, handler: { _ in
|
||||
self.shareCategoryFile(at: index, fileType: .gpx, anchor: anchor)
|
||||
}))
|
||||
let delete = L("delete_list")
|
||||
let deleteAction = UIAlertAction(title: delete, style: .destructive, handler: { [viewModel] _ in
|
||||
viewModel!.deleteCategory(at: index)
|
||||
})
|
||||
deleteAction.isEnabled = (viewModel.canDeleteCategory())
|
||||
actionSheet.addAction(deleteAction)
|
||||
let cancel = L("cancel")
|
||||
actionSheet.addAction(UIAlertAction(title: cancel, style: .cancel, handler: nil))
|
||||
|
||||
present(actionSheet, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func openRecentlyDeleted() {
|
||||
let recentlyDeletedController = RecentlyDeletedCategoriesViewController(viewModel: RecentlyDeletedCategoriesViewModel(bookmarksManager: BookmarksManager.shared()))
|
||||
MapViewController.shared()?.navigationController?.pushViewController(recentlyDeletedController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCViewController: BMCView {
|
||||
func update(sections: [BMCSection]) {
|
||||
if sections.isEmpty {
|
||||
tableView.reloadData()
|
||||
} else {
|
||||
let indexes = IndexSet(sections.map { viewModel.sectionIndex(section: $0) })
|
||||
tableView.update { tableView.reloadSections(indexes, with: .automatic) }
|
||||
}
|
||||
}
|
||||
|
||||
func insert(at indexPaths: [IndexPath]) {
|
||||
tableView.insertRows(at: indexPaths, with: .automatic)
|
||||
}
|
||||
|
||||
func delete(at indexPaths: [IndexPath]) {
|
||||
tableView.deleteRows(at: indexPaths, with: .automatic)
|
||||
}
|
||||
|
||||
func conversionFinished(success: Bool) {
|
||||
MWMAlertViewController.activeAlert().closeAlert {
|
||||
if !success {
|
||||
MWMAlertViewController.activeAlert().presentBookmarkConversionErrorAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCViewController: UITableViewDataSource {
|
||||
func numberOfSections(in _: UITableView) -> Int {
|
||||
return viewModel.numberOfSections()
|
||||
}
|
||||
|
||||
func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch viewModel.sectionType(section: section) {
|
||||
case .categories: fallthrough
|
||||
case .actions, .recentlyDeleted: fallthrough
|
||||
case .notifications: return viewModel.numberOfRows(section: section)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
func dequeCell<Cell>(_ cell: Cell.Type) -> Cell where Cell: UITableViewCell {
|
||||
return tableView.dequeueReusableCell(cell: cell, indexPath: indexPath)
|
||||
}
|
||||
|
||||
switch viewModel.sectionType(section: indexPath.section) {
|
||||
case .categories:
|
||||
return dequeCell(BMCCategoryCell.self).config(category: viewModel.category(at: indexPath.row),
|
||||
delegate: self)
|
||||
case .actions:
|
||||
return dequeCell(BMCActionsCell.self).config(model: viewModel.action(at: indexPath.row))
|
||||
case .recentlyDeleted:
|
||||
return dequeCell(BMCActionsCell.self).config(model: viewModel.recentlyDeletedCategories())
|
||||
case .notifications:
|
||||
return dequeCell(BMCNotificationsCell.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
if viewModel.sectionType(section: indexPath.section) != .categories {
|
||||
return false
|
||||
}
|
||||
|
||||
return viewModel.canDeleteCategory()
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView,
|
||||
commit editingStyle: UITableViewCell.EditingStyle,
|
||||
forRowAt indexPath: IndexPath) {
|
||||
guard editingStyle == .delete,
|
||||
viewModel.sectionType(section: indexPath.section) == .categories else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.deleteCategory(at: indexPath.row)
|
||||
}
|
||||
|
||||
func tableView(_: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
switch viewModel.sectionType(section: section) {
|
||||
case .notifications: fallthrough
|
||||
case .categories: return 48
|
||||
case .actions, .recentlyDeleted: return 24
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
switch viewModel.sectionType(section: section) {
|
||||
case .categories:
|
||||
let categoriesHeader = tableView.dequeueReusableHeaderFooterView(BMCCategoriesHeader.self)
|
||||
categoriesHeader.isShowAll = viewModel.areAllCategoriesHidden()
|
||||
categoriesHeader.title = L("bookmark_lists")
|
||||
categoriesHeader.delegate = self
|
||||
return categoriesHeader
|
||||
case .actions, .recentlyDeleted: return actionsHeader
|
||||
case .notifications: return notificationsHeader
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
switch viewModel.sectionType(section: indexPath.section) {
|
||||
case .categories:
|
||||
openCategory(category: viewModel.category(at: indexPath.row))
|
||||
case .actions:
|
||||
switch viewModel.action(at: indexPath.row) {
|
||||
case .create: createNewCategory()
|
||||
case .exportAll: shareAllCategories(anchor: tableView.cellForRow(at: indexPath))
|
||||
case .import: showImportDialog()
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
case .recentlyDeleted: openRecentlyDeleted()
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCViewController: BMCCategoryCellDelegate {
|
||||
func cell(_ cell: BMCCategoryCell, didCheck visible: Bool) {
|
||||
guard let indexPath = tableView.indexPath(for: cell) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
setCategoryVisible(visible, at: indexPath.row)
|
||||
}
|
||||
|
||||
func cell(_ cell: BMCCategoryCell, didPress moreButton: UIButton) {
|
||||
guard let indexPath = tableView.indexPath(for: cell) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
editCategory(at: indexPath.row, anchor: moreButton)
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCViewController: BMCCategoriesHeaderDelegate {
|
||||
func visibilityAction(_ categoriesHeader: BMCCategoriesHeader) {
|
||||
viewModel.updateAllCategoriesVisibility(isShowAll: categoriesHeader.isShowAll)
|
||||
categoriesHeader.isShowAll = viewModel.areAllCategoriesHidden()
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCViewController: CategorySettingsViewControllerDelegate {
|
||||
func categorySettingsController(_ viewController: CategorySettingsViewController,
|
||||
didEndEditing categoryId: MWMMarkGroupID) {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func categorySettingsController(_ viewController: CategorySettingsViewController,
|
||||
didDelete categoryId: MWMMarkGroupID) {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCViewController: BookmarksListDelegate {
|
||||
func bookmarksListDidDeleteGroup() {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="BMCViewController" customModule="CoMaps" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="actionsHeader" destination="DhR-7O-ccQ" id="PPD-Ov-b69"/>
|
||||
<outlet property="notificationsHeader" destination="G0o-Op-zPp" id="PUK-3H-1q3"/>
|
||||
<outlet property="tableView" destination="2ia-hi-UhQ" id="qJG-eV-PoF"/>
|
||||
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" canCancelContentTouches="NO" style="grouped" separatorStyle="default" allowsSelectionDuringEditing="YES" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="-1" estimatedSectionHeaderHeight="-1" sectionFooterHeight="1" estimatedSectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="2ia-hi-UhQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<color key="backgroundColor" systemColor="groupTableViewBackgroundColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="TableView:PressBackground"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-1" id="P7h-mr-lVO"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" secondItem="2ia-hi-UhQ" secondAttribute="bottom" id="3LS-kF-koO"/>
|
||||
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="2ia-hi-UhQ" secondAttribute="trailing" id="Xvt-xX-QwP"/>
|
||||
<constraint firstItem="2ia-hi-UhQ" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="fEc-XL-6gs"/>
|
||||
<constraint firstItem="2ia-hi-UhQ" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" id="zOH-o1-384"/>
|
||||
</constraints>
|
||||
<point key="canvasLocation" x="10" y="54"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" id="DhR-7O-ccQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<viewLayoutGuide key="safeArea" id="Iuf-Mb-Pmk"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="483" y="-50"/>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" id="G0o-Op-zPp" customClass="BMCNotificationsHeader" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="48"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="WYb-xC-cJR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="48"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BbP-rE-XH6">
|
||||
<rect key="frame" x="16" y="13.5" width="343" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="medium14:blackSecondaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="BbP-rE-XH6" secondAttribute="trailing" constant="16" id="0y9-8D-etT"/>
|
||||
<constraint firstItem="BbP-rE-XH6" firstAttribute="centerY" secondItem="WYb-xC-cJR" secondAttribute="centerY" id="E2o-Zj-Sfl"/>
|
||||
<constraint firstItem="BbP-rE-XH6" firstAttribute="leading" secondItem="WYb-xC-cJR" secondAttribute="leading" constant="16" id="NL9-Mg-ULl"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="Gks-qZ-sxu"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="WYb-xC-cJR" firstAttribute="top" secondItem="G0o-Op-zPp" secondAttribute="top" id="HYA-0D-zWS"/>
|
||||
<constraint firstItem="WYb-xC-cJR" firstAttribute="leading" secondItem="Gks-qZ-sxu" secondAttribute="leading" id="KA1-YB-hBF"/>
|
||||
<constraint firstItem="WYb-xC-cJR" firstAttribute="trailing" secondItem="Gks-qZ-sxu" secondAttribute="trailing" id="hne-ZG-GAn"/>
|
||||
<constraint firstAttribute="bottom" secondItem="WYb-xC-cJR" secondAttribute="bottom" id="vwe-2c-cjW"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="label" destination="BbP-rE-XH6" id="UTL-17-qdv"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="483" y="35"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="groupTableViewBackgroundColor">
|
||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
protocol BMCView: AnyObject {
|
||||
func update(sections: [BMCSection])
|
||||
func delete(at indexPaths: [IndexPath])
|
||||
func insert(at indexPaths: [IndexPath])
|
||||
func conversionFinished(success: Bool)
|
||||
}
|
||||
|
||||
final class BMCDefaultViewModel: NSObject {
|
||||
private let manager = BookmarksManager.shared()
|
||||
|
||||
weak var view: BMCView?
|
||||
|
||||
private var sections: [BMCSection] = []
|
||||
private var categories: [BookmarkGroup] = []
|
||||
private var actions: [BMCAction] = []
|
||||
private var notifications: [BMCNotification] = []
|
||||
|
||||
private(set) var isPendingPermission = false
|
||||
private var isAuthenticated = false
|
||||
private var filesPrepared = false
|
||||
|
||||
let minCategoryNameLength: UInt = 0
|
||||
let maxCategoryNameLength: UInt = 60
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
reloadData()
|
||||
}
|
||||
|
||||
private func getCategories() -> [BookmarkGroup] {
|
||||
manager.sortedUserCategories()
|
||||
}
|
||||
|
||||
private func getActions() -> [BMCAction] {
|
||||
var actions: [BMCAction] = [.create]
|
||||
actions.append(.import)
|
||||
if !manager.areAllCategoriesEmpty() {
|
||||
actions.append(.exportAll)
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
private func getNotifications() -> [BMCNotification] {
|
||||
[.load]
|
||||
}
|
||||
|
||||
func reloadData() {
|
||||
sections.removeAll()
|
||||
|
||||
if manager.areBookmarksLoaded() {
|
||||
sections.append(.categories)
|
||||
categories = getCategories()
|
||||
|
||||
sections.append(.actions)
|
||||
actions = getActions()
|
||||
|
||||
if manager.recentlyDeletedCategoriesCount() != .zero {
|
||||
sections.append(.recentlyDeleted)
|
||||
}
|
||||
} else {
|
||||
sections.append(.notifications)
|
||||
notifications = getNotifications()
|
||||
}
|
||||
|
||||
view?.update(sections: [])
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCDefaultViewModel {
|
||||
func numberOfSections() -> Int {
|
||||
sections.count
|
||||
}
|
||||
|
||||
func sectionType(section: Int) -> BMCSection {
|
||||
sections[section]
|
||||
}
|
||||
|
||||
func sectionIndex(section: BMCSection) -> Int {
|
||||
sections.firstIndex(of: section)!
|
||||
}
|
||||
|
||||
func numberOfRows(section: Int) -> Int {
|
||||
numberOfRows(section: sectionType(section: section))
|
||||
}
|
||||
|
||||
func numberOfRows(section: BMCSection) -> Int {
|
||||
switch section {
|
||||
case .categories: return categories.count
|
||||
case .actions: return actions.count
|
||||
case .recentlyDeleted: return 1
|
||||
case .notifications: return notifications.count
|
||||
}
|
||||
}
|
||||
|
||||
func category(at index: Int) -> BookmarkGroup {
|
||||
categories[index]
|
||||
}
|
||||
|
||||
func canDeleteCategory() -> Bool {
|
||||
categories.count > 1
|
||||
}
|
||||
|
||||
func action(at index: Int) -> BMCAction {
|
||||
actions[index]
|
||||
}
|
||||
|
||||
func recentlyDeletedCategories() -> BMCAction {
|
||||
.recentlyDeleted(Int(manager.recentlyDeletedCategoriesCount()))
|
||||
}
|
||||
|
||||
func notification(at index: Int) -> BMCNotification {
|
||||
notifications[index]
|
||||
}
|
||||
|
||||
func areAllCategoriesHidden() -> Bool {
|
||||
var result = true
|
||||
categories.forEach { if $0.isVisible { result = false } }
|
||||
return result
|
||||
}
|
||||
|
||||
func updateAllCategoriesVisibility(isShowAll: Bool) {
|
||||
manager.setUserCategoriesVisible(isShowAll)
|
||||
}
|
||||
|
||||
func addCategory(name: String) {
|
||||
guard let section = sections.firstIndex(of: .categories) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
categories.insert(manager.category(withId: manager.createCategory(withName: name)), at: 0)
|
||||
view?.insert(at: [IndexPath(row: 0, section: section)])
|
||||
}
|
||||
|
||||
func deleteCategory(at index: Int) {
|
||||
guard let section = sections.firstIndex(of: .categories) else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
let category = categories[index]
|
||||
categories.remove(at: index)
|
||||
view?.delete(at: [IndexPath(row: index, section: section)])
|
||||
manager.deleteCategory(category.categoryId)
|
||||
}
|
||||
|
||||
func checkCategory(name: String) -> Bool {
|
||||
manager.checkCategoryName(name)
|
||||
}
|
||||
|
||||
func shareCategoryFile(at index: Int, fileType: KmlFileType, handler: @escaping SharingResultCompletionHandler) {
|
||||
let category = categories[index]
|
||||
manager.shareCategory(category.categoryId, fileType: fileType, completion: handler)
|
||||
}
|
||||
|
||||
func shareAllCategories(handler: @escaping SharingResultCompletionHandler) {
|
||||
manager.shareAllCategories(completion: handler)
|
||||
}
|
||||
|
||||
func importCategories(from urls: [URL]) {
|
||||
// TODO: Refactor this call when the multiple files parsing support will be added to the bookmark_manager.
|
||||
urls.forEach(manager.loadBookmarkFile(_:))
|
||||
}
|
||||
|
||||
func finishShareCategory() {
|
||||
manager.finishSharing()
|
||||
}
|
||||
|
||||
func addToObserverList() {
|
||||
manager.add(self)
|
||||
}
|
||||
|
||||
func removeFromObserverList() {
|
||||
manager.remove(self)
|
||||
}
|
||||
|
||||
func setNotificationsEnabled(_ enabled: Bool) {
|
||||
manager.setNotificationsEnabled(enabled)
|
||||
}
|
||||
|
||||
func areNotificationsEnabled() -> Bool {
|
||||
manager.areNotificationsEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
extension BMCDefaultViewModel: BookmarksObserver {
|
||||
func onBookmarksLoadFinished() {
|
||||
reloadData()
|
||||
}
|
||||
|
||||
func onBookmarksCategoryDeleted(_ groupId: MWMMarkGroupID) {
|
||||
reloadData()
|
||||
}
|
||||
|
||||
func onBookmarkDeleted(_: MWMMarkID) {
|
||||
reloadData()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
struct BMCDefaultViewModel: BMCViewModel {
|
||||
private var sections: [BMCSection] = [.permissions, .categoriesList, .creation]
|
||||
|
||||
func numberOfSections() -> Int {
|
||||
return sections.count
|
||||
}
|
||||
|
||||
func sectionType(section: Int) -> BMCSection {
|
||||
return sections[section]
|
||||
}
|
||||
|
||||
func sectionIndex(section: BMCSection) -> Int {
|
||||
return sections.index(of: section)!
|
||||
}
|
||||
|
||||
func numberOfRows(section _: Int) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func item(indexPath: IndexPath) -> BMCModel {
|
||||
switch sectionType(section: indexPath.section) {
|
||||
case .permissions: return BMCPermission.signup
|
||||
case .categoriesList: return BMCCategory()
|
||||
case .creation: return BMCAction.create
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="BMCActionCell" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="113"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="112.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="y7C-Jk-OIm">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="112.5"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h0a-ss-bey">
|
||||
<rect key="frame" x="16" y="16" width="288" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Zb4-aL-Ldq">
|
||||
<rect key="frame" x="16" y="52.5" width="288" height="40"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="40" id="Dik-ca-lyT"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Button"/>
|
||||
<connections>
|
||||
<action selector="buttonAction" destination="KGk-i7-Jjw" eventType="touchUpInside" id="vVV-Mk-uw3"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="h0a-ss-bey" secondAttribute="trailing" constant="16" id="FFb-bX-gJB"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Zb4-aL-Ldq" secondAttribute="bottom" constant="20" id="WkL-YA-31h"/>
|
||||
<constraint firstItem="Zb4-aL-Ldq" firstAttribute="leading" secondItem="y7C-Jk-OIm" secondAttribute="leading" constant="16" id="am3-bb-og1"/>
|
||||
<constraint firstItem="h0a-ss-bey" firstAttribute="leading" secondItem="y7C-Jk-OIm" secondAttribute="leading" constant="16" id="eDv-Er-8Pb"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Zb4-aL-Ldq" secondAttribute="trailing" constant="16" id="eUB-Wu-h3R"/>
|
||||
<constraint firstItem="h0a-ss-bey" firstAttribute="top" secondItem="y7C-Jk-OIm" secondAttribute="top" constant="16" id="gvx-dV-zx1"/>
|
||||
<constraint firstItem="Zb4-aL-Ldq" firstAttribute="top" secondItem="h0a-ss-bey" secondAttribute="bottom" constant="16" id="sNk-mv-NuJ"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="y7C-Jk-OIm" secondAttribute="trailing" id="A6B-j1-qyA"/>
|
||||
<constraint firstItem="y7C-Jk-OIm" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="Raz-jA-KQq"/>
|
||||
<constraint firstAttribute="bottom" secondItem="y7C-Jk-OIm" secondAttribute="bottom" id="Tvc-br-Rug"/>
|
||||
<constraint firstItem="y7C-Jk-OIm" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="yaO-ZP-BVr"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="button" destination="Zb4-aL-Ldq" id="Sgz-Oh-anC"/>
|
||||
<outlet property="label" destination="h0a-ss-bey" id="LbI-bT-OKg"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
protocol BMCCategoriesHeaderDelegate: AnyObject {
|
||||
func visibilityAction(_ categoriesHeader: BMCCategoriesHeader)
|
||||
}
|
||||
|
||||
final class BMCCategoriesHeader: UITableViewHeaderFooterView {
|
||||
@IBOutlet private weak var label: UILabel!
|
||||
@IBOutlet private weak var button: UIButton!
|
||||
|
||||
var isShowAll = false {
|
||||
didSet {
|
||||
let title = L(isShowAll ? "bookmark_lists_show_all" : "bookmark_lists_hide_all")
|
||||
UIView.performWithoutAnimation {
|
||||
button.setTitle(title, for: .normal)
|
||||
button.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var title: String? {
|
||||
didSet {
|
||||
title = title?.uppercased()
|
||||
label.text = title
|
||||
}
|
||||
}
|
||||
|
||||
weak var delegate: BMCCategoriesHeaderDelegate?
|
||||
|
||||
@IBAction private func buttonAction() {
|
||||
delegate?.visibilityAction(self)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="hpL-he-7N6" customClass="BMCCategoriesHeader" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="48"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qNi-Yh-Bat">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="48"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="PGC-cy-M2K">
|
||||
<rect key="frame" x="16" y="13.5" width="42" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="medium14:blackSecondaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="3hn-pU-MI1">
|
||||
<rect key="frame" x="302" y="0.0" width="73" height="48"/>
|
||||
<inset key="contentEdgeInsets" minX="0.0" minY="0.0" maxX="14" maxY="0.0"/>
|
||||
<state key="normal" title="Show All"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="linkBlueText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="buttonAction" destination="hpL-he-7N6" eventType="touchUpInside" id="mQo-HU-GKM"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="PGC-cy-M2K" firstAttribute="leading" secondItem="qNi-Yh-Bat" secondAttribute="leading" constant="16" id="2OC-uL-Mgr"/>
|
||||
<constraint firstItem="PGC-cy-M2K" firstAttribute="centerY" secondItem="qNi-Yh-Bat" secondAttribute="centerY" id="44v-Uo-bj7"/>
|
||||
<constraint firstAttribute="trailing" secondItem="3hn-pU-MI1" secondAttribute="trailing" id="HNq-6x-QDD"/>
|
||||
<constraint firstAttribute="bottom" secondItem="3hn-pU-MI1" secondAttribute="bottom" id="VCh-vb-yPn"/>
|
||||
<constraint firstItem="3hn-pU-MI1" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="PGC-cy-M2K" secondAttribute="trailing" constant="4" id="hGm-gr-ABM"/>
|
||||
<constraint firstItem="3hn-pU-MI1" firstAttribute="top" secondItem="qNi-Yh-Bat" secondAttribute="top" id="thZ-ah-uC2"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="aw1-gI-QsW" firstAttribute="trailing" secondItem="qNi-Yh-Bat" secondAttribute="trailing" id="4t4-Hg-Xce"/>
|
||||
<constraint firstItem="aw1-gI-QsW" firstAttribute="bottom" secondItem="qNi-Yh-Bat" secondAttribute="bottom" id="Ptg-FC-sav"/>
|
||||
<constraint firstItem="qNi-Yh-Bat" firstAttribute="leading" secondItem="aw1-gI-QsW" secondAttribute="leading" id="XuM-Ot-YqM"/>
|
||||
<constraint firstItem="qNi-Yh-Bat" firstAttribute="top" secondItem="aw1-gI-QsW" secondAttribute="top" id="cp0-ul-dGE"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<viewLayoutGuide key="safeArea" id="aw1-gI-QsW"/>
|
||||
<connections>
|
||||
<outlet property="button" destination="3hn-pU-MI1" id="aAd-Ov-dIM"/>
|
||||
<outlet property="label" destination="PGC-cy-M2K" id="D2m-ae-X57"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="483" y="-148"/>
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
protocol BMCCategoryCellDelegate: AnyObject {
|
||||
func cell(_ cell: BMCCategoryCell, didCheck visible: Bool)
|
||||
func cell(_ cell: BMCCategoryCell, didPress moreButton: UIButton)
|
||||
}
|
||||
|
||||
final class BMCCategoryCell: MWMTableViewCell {
|
||||
@IBOutlet private weak var titleLabel: UILabel!
|
||||
@IBOutlet private weak var subtitleLabel: UILabel!
|
||||
|
||||
@IBOutlet private weak var moreButton: UIButton! {
|
||||
didSet {
|
||||
moreButton.setImage(#imageLiteral(resourceName: "ic24PxMore"), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet weak var visibleCheckmark: Checkmark!
|
||||
|
||||
private var category: BookmarkGroup? {
|
||||
didSet {
|
||||
categoryUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
private weak var delegate: BMCCategoryCellDelegate?
|
||||
|
||||
func config(category: BookmarkGroup, delegate: BMCCategoryCellDelegate) -> UITableViewCell {
|
||||
self.category = category
|
||||
self.delegate = delegate
|
||||
return self
|
||||
}
|
||||
|
||||
@IBAction func onVisibleChanged(_ sender: Checkmark) {
|
||||
delegate?.cell(self, didCheck: sender.isChecked)
|
||||
}
|
||||
|
||||
@IBAction private func moreAction() {
|
||||
delegate?.cell(self, didPress: moreButton)
|
||||
}
|
||||
|
||||
func categoryUpdated() {
|
||||
guard let category = category else { return }
|
||||
titleLabel.text = category.title
|
||||
subtitleLabel.text = category.placesCountTitle()
|
||||
visibleCheckmark.isChecked = category.isVisible
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19162" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19144"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="BMCCategoryCell" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wcq-KH-q74" customClass="Checkmark" customModule="CoMaps" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="56" height="60.5"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="56" id="iRO-vl-eYM"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="image" keyPath="offImage" value="ic_eye_off"/>
|
||||
<userDefinedRuntimeAttribute type="image" keyPath="onImage" value="ic_eye_on"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="onVisibleChanged:" destination="KGk-i7-Jjw" eventType="valueChanged" id="fV8-pr-hNc"/>
|
||||
</connections>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="252" verticalCompressionResistancePriority="751" text="My Places" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jut-eq-wia">
|
||||
<rect key="frame" x="56" y="10" width="204" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular16:blackPrimaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Public • 12 Bookmarks" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jBd-Tj-RiW">
|
||||
<rect key="frame" x="56" y="35" width="204" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<color key="textColor" white="0.0" alpha="0.5781785102739726" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular14:blackSecondaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2gi-fk-gR6">
|
||||
<rect key="frame" x="264" y="0.0" width="56" height="60"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="56" id="ot8-q5-ynR"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MWMGray"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
<connections>
|
||||
<action selector="moreAction" destination="KGk-i7-Jjw" eventType="touchUpInside" id="zmP-yn-CEM"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="jBd-Tj-RiW" firstAttribute="leading" secondItem="wcq-KH-q74" secondAttribute="trailing" id="BBj-bB-xCt"/>
|
||||
<constraint firstAttribute="trailing" secondItem="2gi-fk-gR6" secondAttribute="trailing" id="Ddb-qq-tEN"/>
|
||||
<constraint firstAttribute="bottom" secondItem="jBd-Tj-RiW" secondAttribute="bottom" constant="10" id="ITj-Sq-UKz"/>
|
||||
<constraint firstAttribute="bottom" secondItem="wcq-KH-q74" secondAttribute="bottom" constant="-0.5" id="LnB-MK-oB3"/>
|
||||
<constraint firstItem="jut-eq-wia" firstAttribute="leading" secondItem="wcq-KH-q74" secondAttribute="trailing" id="RBv-Jh-88n"/>
|
||||
<constraint firstItem="jBd-Tj-RiW" firstAttribute="top" secondItem="jut-eq-wia" secondAttribute="bottom" constant="4" id="TT5-VN-D7j"/>
|
||||
<constraint firstItem="2gi-fk-gR6" firstAttribute="leading" secondItem="jut-eq-wia" secondAttribute="trailing" constant="4" id="Xx2-s7-Jt0"/>
|
||||
<constraint firstItem="2gi-fk-gR6" firstAttribute="leading" secondItem="jBd-Tj-RiW" secondAttribute="trailing" constant="4" id="b4E-PQ-Lca"/>
|
||||
<constraint firstItem="wcq-KH-q74" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="fYG-79-pgs"/>
|
||||
<constraint firstItem="wcq-KH-q74" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="lui-lx-nl7"/>
|
||||
<constraint firstAttribute="bottom" secondItem="2gi-fk-gR6" secondAttribute="bottom" id="maE-v3-UlZ"/>
|
||||
<constraint firstItem="2gi-fk-gR6" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="nbD-ba-3tr"/>
|
||||
<constraint firstItem="jut-eq-wia" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="10" id="omd-cs-RXb"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="moreButton" destination="2gi-fk-gR6" id="t3r-XO-zDV"/>
|
||||
<outlet property="subtitleLabel" destination="jBd-Tj-RiW" id="D3j-45-I9U"/>
|
||||
<outlet property="titleLabel" destination="jut-eq-wia" id="pHy-5L-bhq"/>
|
||||
<outlet property="visibleCheckmark" destination="wcq-KH-q74" id="X4U-VF-MLB"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="52.799999999999997" y="47.676161919040482"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="ic_eye_off" width="24" height="24"/>
|
||||
<image name="ic_eye_on" width="24" height="24"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
@objc protocol CategorySettingsViewControllerDelegate: AnyObject {
|
||||
func categorySettingsController(_ viewController: CategorySettingsViewController,
|
||||
didEndEditing categoryId: MWMMarkGroupID)
|
||||
func categorySettingsController(_ viewController: CategorySettingsViewController,
|
||||
didDelete categoryId: MWMMarkGroupID)
|
||||
}
|
||||
|
||||
final class CategorySettingsViewController: MWMTableViewController {
|
||||
private enum Sections: Int {
|
||||
case info
|
||||
case description
|
||||
case delete
|
||||
case count
|
||||
}
|
||||
|
||||
private enum InfoSectionRows: Int {
|
||||
case title
|
||||
}
|
||||
|
||||
private let bookmarkGroup: BookmarkGroup
|
||||
private var noteCell: MWMNoteCell?
|
||||
private var changesMade = false
|
||||
private var newName: String?
|
||||
private var newAnnotation: String?
|
||||
|
||||
@objc weak var delegate: CategorySettingsViewControllerDelegate?
|
||||
|
||||
@objc init(bookmarkGroup: BookmarkGroup) {
|
||||
self.bookmarkGroup = bookmarkGroup
|
||||
super.init(style: .grouped)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
title = L("edit")
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save,
|
||||
target: self,
|
||||
action: #selector(onSave))
|
||||
|
||||
tableView.registerNib(cell: BookmarkTitleCell.self)
|
||||
tableView.registerNib(cell: MWMButtonCell.self)
|
||||
tableView.registerNib(cell: MWMNoteCell.self)
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
Sections.count.rawValue
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch Sections(rawValue: section) {
|
||||
case .info:
|
||||
return 1
|
||||
case .description, .delete:
|
||||
return 1
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
switch Sections(rawValue: indexPath.section) {
|
||||
case .info:
|
||||
switch InfoSectionRows(rawValue: indexPath.row) {
|
||||
case .title:
|
||||
let cell = tableView.dequeueReusableCell(cell: BookmarkTitleCell.self, indexPath: indexPath)
|
||||
cell.configure(name: bookmarkGroup.title, delegate: self, hint: L("bookmarks_error_message_empty_list_name"))
|
||||
return cell
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
case .description:
|
||||
if let noteCell = noteCell {
|
||||
return noteCell
|
||||
} else {
|
||||
let cell = tableView.dequeueReusableCell(cell: MWMNoteCell.self, indexPath: indexPath)
|
||||
cell.config(with: self, noteText: bookmarkGroup.detailedAnnotation,
|
||||
placeholder: L("placepage_personal_notes_hint"))
|
||||
noteCell = cell
|
||||
return cell
|
||||
}
|
||||
case .delete:
|
||||
let cell = tableView.dequeueReusableCell(cell: MWMButtonCell.self, indexPath: indexPath)
|
||||
cell.configure(with: self,
|
||||
title: L("delete_list"),
|
||||
enabled: BookmarksManager.shared().userCategoriesCount() > 1)
|
||||
return cell
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func onSave() {
|
||||
view.endEditing(true)
|
||||
if let newName = newName, !newName.isEmpty {
|
||||
BookmarksManager.shared().setCategory(bookmarkGroup.categoryId, name: newName)
|
||||
changesMade = true
|
||||
}
|
||||
|
||||
if let newAnnotation = newAnnotation {
|
||||
BookmarksManager.shared().setCategory(bookmarkGroup.categoryId, description: newAnnotation)
|
||||
changesMade = true
|
||||
}
|
||||
|
||||
delegate?.categorySettingsController(self, didEndEditing: bookmarkGroup.categoryId)
|
||||
}
|
||||
}
|
||||
|
||||
extension CategorySettingsViewController: BookmarkTitleCellDelegate {
|
||||
func didFinishEditingTitle(_ title: String) {
|
||||
newName = title
|
||||
}
|
||||
}
|
||||
|
||||
extension CategorySettingsViewController: MWMNoteCellDelegate {
|
||||
func cell(_ cell: MWMNoteCell, didChangeSizeAndText text: String) {
|
||||
UIView.setAnimationsEnabled(false)
|
||||
tableView.refresh()
|
||||
UIView.setAnimationsEnabled(true)
|
||||
tableView.scrollToRow(at: IndexPath(item: 0, section: Sections.description.rawValue),
|
||||
at: .bottom,
|
||||
animated: true)
|
||||
}
|
||||
|
||||
func cell(_ cell: MWMNoteCell, didFinishEditingWithText text: String) {
|
||||
newAnnotation = text
|
||||
}
|
||||
}
|
||||
|
||||
extension CategorySettingsViewController: MWMButtonCellDelegate {
|
||||
func cellDidPressButton(_ cell: UITableViewCell) {
|
||||
BookmarksManager.shared().deleteCategory(bookmarkGroup.categoryId)
|
||||
delegate?.categorySettingsController(self, didDelete: bookmarkGroup.categoryId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
final class BMCNotificationsCell: MWMTableViewCell {
|
||||
@IBOutlet private weak var spinner: UIView! {
|
||||
didSet {
|
||||
circularProgress = MWMCircularProgress(parentView: spinner)
|
||||
circularProgress.state = .spinner
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet private weak var label: UILabel! {
|
||||
didSet {
|
||||
label.text = L("load_kmz_title")
|
||||
}
|
||||
}
|
||||
|
||||
private var circularProgress: MWMCircularProgress!
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="BMCNotificationsCell" customModule="CoMaps" customModuleProvider="target" propertyAccessControl="none">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="48"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="48"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="23z-Ov-GrT">
|
||||
<rect key="frame" x="16" y="12" width="24" height="24"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="24" id="Duf-kw-YwR"/>
|
||||
<constraint firstAttribute="height" constant="24" id="o5g-od-uLZ"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ydm-M6-i2D">
|
||||
<rect key="frame" x="56" y="13.5" width="233" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular16:blackSecondaryText"/>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="ydm-M6-i2D" firstAttribute="leading" secondItem="23z-Ov-GrT" secondAttribute="trailing" constant="16" id="IFP-zv-ycJ"/>
|
||||
<constraint firstItem="ydm-M6-i2D" firstAttribute="centerY" secondItem="23z-Ov-GrT" secondAttribute="centerY" id="XQ3-Ic-lXU"/>
|
||||
<constraint firstItem="23z-Ov-GrT" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="bIW-Rw-EGD"/>
|
||||
<constraint firstItem="23z-Ov-GrT" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="er5-i4-cs1"/>
|
||||
<constraint firstAttribute="bottom" secondItem="23z-Ov-GrT" secondAttribute="bottom" constant="12" id="gkY-we-imN"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="ydm-M6-i2D" secondAttribute="trailing" constant="16" id="nAH-MD-erS"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="label" destination="ydm-M6-i2D" id="FJz-8r-GV0"/>
|
||||
<outlet property="spinner" destination="23z-Ov-GrT" id="9Me-CJ-fX6"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="139" y="154"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
</document>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
final class BMCNotificationsHeader: UIView {
|
||||
@IBOutlet private weak var label: UILabel! {
|
||||
didSet {
|
||||
label.text = L("bookmark_lists").uppercased()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
final class RecentlyDeletedCategoriesViewController: MWMViewController {
|
||||
|
||||
private enum LocalizedStrings {
|
||||
static let clear = L("clear")
|
||||
static let delete = L("delete")
|
||||
static let deleteAll = L("delete_all")
|
||||
static let recover = L("recover")
|
||||
static let recoverAll = L("recover_all")
|
||||
static let recentlyDeleted = L("bookmarks_recently_deleted")
|
||||
static let searchInTheList = L("search_in_the_list")
|
||||
}
|
||||
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
|
||||
private lazy var clearButton = UIBarButtonItem(title: LocalizedStrings.clear, style: .done, target: self, action: #selector(clearButtonDidTap))
|
||||
private lazy var recoverButton = UIBarButtonItem(title: LocalizedStrings.recover, style: .done, target: self, action: #selector(recoverButtonDidTap))
|
||||
private lazy var deleteButton = UIBarButtonItem(title: LocalizedStrings.delete, style: .done, target: self, action: #selector(deleteButtonDidTap))
|
||||
private let searchController = UISearchController(searchResultsController: nil)
|
||||
private let viewModel: RecentlyDeletedCategoriesViewModel
|
||||
|
||||
init(viewModel: RecentlyDeletedCategoriesViewModel = RecentlyDeletedCategoriesViewModel(bookmarksManager: BookmarksManager.shared())) {
|
||||
self.viewModel = viewModel
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
viewModel.stateDidChange = { [weak self] state in
|
||||
self?.updateState(state)
|
||||
}
|
||||
viewModel.filteredDataSourceDidChange = { [weak self] dataSource in
|
||||
guard let self else { return }
|
||||
if dataSource.isEmpty {
|
||||
self.tableView.reloadData()
|
||||
} else {
|
||||
let indexes = IndexSet(integersIn: 0...dataSource.count - 1)
|
||||
self.tableView.update { self.tableView.reloadSections(indexes, with: .automatic) }
|
||||
}
|
||||
}
|
||||
viewModel.onCategoriesIsEmpty = { [weak self] in
|
||||
self?.goBack()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupView()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
navigationController?.setToolbarHidden(true, animated: true)
|
||||
}
|
||||
|
||||
private func setupView() {
|
||||
extendedLayoutIncludesOpaqueBars = true
|
||||
setupNavigationBar()
|
||||
setupToolBar()
|
||||
setupSearchBar()
|
||||
setupTableView()
|
||||
layout()
|
||||
updateState(viewModel.state)
|
||||
}
|
||||
|
||||
private func setupNavigationBar() {
|
||||
title = LocalizedStrings.recentlyDeleted
|
||||
}
|
||||
|
||||
private func setupToolBar() {
|
||||
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
toolbarItems = [flexibleSpace, recoverButton, flexibleSpace, deleteButton, flexibleSpace]
|
||||
navigationController?.isToolbarHidden = false
|
||||
}
|
||||
|
||||
private func setupSearchBar() {
|
||||
searchController.searchBar.placeholder = LocalizedStrings.searchInTheList
|
||||
searchController.obscuresBackgroundDuringPresentation = false
|
||||
searchController.hidesNavigationBarDuringPresentation = false
|
||||
searchController.searchBar.delegate = self
|
||||
searchController.searchBar.applyTheme()
|
||||
navigationItem.searchController = searchController
|
||||
navigationItem.hidesSearchBarWhenScrolling = true
|
||||
}
|
||||
|
||||
private func setupTableView() {
|
||||
tableView.setStyles([.tableView, .pressBackground])
|
||||
tableView.allowsMultipleSelectionDuringEditing = true
|
||||
tableView.register(cell: RecentlyDeletedTableViewCell.self)
|
||||
tableView.setEditing(true, animated: false)
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
}
|
||||
|
||||
private func layout() {
|
||||
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)
|
||||
])
|
||||
}
|
||||
|
||||
private func updateState(_ state: RecentlyDeletedCategoriesViewModel.State) {
|
||||
switch state {
|
||||
case .searching:
|
||||
navigationController?.setToolbarHidden(true, animated: false)
|
||||
searchController.searchBar.isUserInteractionEnabled = true
|
||||
case .nothingSelected:
|
||||
navigationController?.setToolbarHidden(false, animated: false)
|
||||
recoverButton.title = LocalizedStrings.recoverAll
|
||||
deleteButton.title = LocalizedStrings.deleteAll
|
||||
searchController.searchBar.isUserInteractionEnabled = true
|
||||
navigationItem.rightBarButtonItem = nil
|
||||
tableView.indexPathsForSelectedRows?.forEach { tableView.deselectRow(at: $0, animated: true)}
|
||||
case .someSelected:
|
||||
navigationController?.setToolbarHidden(false, animated: false)
|
||||
recoverButton.title = LocalizedStrings.recover
|
||||
deleteButton.title = LocalizedStrings.delete
|
||||
searchController.searchBar.isUserInteractionEnabled = false
|
||||
navigationItem.rightBarButtonItem = clearButton
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
@objc private func clearButtonDidTap() {
|
||||
viewModel.cancelSelecting()
|
||||
}
|
||||
|
||||
@objc private func recoverButtonDidTap() {
|
||||
viewModel.recoverSelectedCategories()
|
||||
}
|
||||
|
||||
@objc private func deleteButtonDidTap() {
|
||||
viewModel.deleteSelectedCategories()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
extension RecentlyDeletedCategoriesViewController: UITableViewDataSource {
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
viewModel.filteredDataSource.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
viewModel.filteredDataSource[section].content.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(cell: RecentlyDeletedTableViewCell.self, indexPath: indexPath)
|
||||
let category = viewModel.filteredDataSource[indexPath.section].content[indexPath.row]
|
||||
cell.configureWith(RecentlyDeletedTableViewCell.ViewModel(category))
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension RecentlyDeletedCategoriesViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard tableView.isEditing else {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
return
|
||||
}
|
||||
viewModel.selectCategory(at: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
|
||||
guard tableView.isEditing else { return }
|
||||
guard let selectedIndexPaths = tableView.indexPathsForSelectedRows, !selectedIndexPaths.isEmpty else {
|
||||
viewModel.deselectAllCategories()
|
||||
return
|
||||
}
|
||||
viewModel.deselectCategory(at: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let deleteAction = UIContextualAction(style: .destructive, title: LocalizedStrings.delete) { [weak self] (_, _, completion) in
|
||||
self?.viewModel.deleteCategory(at: indexPath)
|
||||
completion(true)
|
||||
}
|
||||
let recoverAction = UIContextualAction(style: .normal, title: LocalizedStrings.recover) { [weak self] (_, _, completion) in
|
||||
self?.viewModel.recoverCategory(at: indexPath)
|
||||
completion(true)
|
||||
}
|
||||
return UISwipeActionsConfiguration(actions: [deleteAction, recoverAction])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UISearchBarDelegate
|
||||
extension RecentlyDeletedCategoriesViewController: UISearchBarDelegate {
|
||||
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
|
||||
searchBar.setShowsCancelButton(true, animated: true)
|
||||
viewModel.startSearching()
|
||||
}
|
||||
|
||||
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
|
||||
searchBar.setShowsCancelButton(false, animated: true)
|
||||
}
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.text = nil
|
||||
searchBar.resignFirstResponder()
|
||||
viewModel.cancelSearching()
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
viewModel.search(searchText)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
final class RecentlyDeletedCategoriesViewModel: NSObject {
|
||||
|
||||
typealias BookmarksManager = RecentlyDeletedCategoriesManager & BookmarksObservable
|
||||
|
||||
enum Section: CaseIterable {
|
||||
struct Model: Equatable {
|
||||
var content: [RecentlyDeletedCategory]
|
||||
}
|
||||
|
||||
case main
|
||||
}
|
||||
|
||||
enum State {
|
||||
case searching
|
||||
case nothingSelected
|
||||
case someSelected
|
||||
}
|
||||
|
||||
private var recentlyDeletedCategoriesManager: BookmarksManager
|
||||
private var dataSource: [Section.Model] = [] {
|
||||
didSet {
|
||||
if dataSource.isEmpty {
|
||||
onCategoriesIsEmpty?()
|
||||
}
|
||||
}
|
||||
}
|
||||
private(set) var state: State = .nothingSelected
|
||||
private(set) var filteredDataSource: [Section.Model] = []
|
||||
private(set) var selectedIndexPaths: [IndexPath] = []
|
||||
private(set) var searchText = String()
|
||||
|
||||
var stateDidChange: ((State) -> Void)?
|
||||
var filteredDataSourceDidChange: (([Section.Model]) -> Void)?
|
||||
var onCategoriesIsEmpty: (() -> Void)?
|
||||
|
||||
init(bookmarksManager: BookmarksManager) {
|
||||
self.recentlyDeletedCategoriesManager = bookmarksManager
|
||||
super.init()
|
||||
subscribeOnBookmarksManagerNotifications()
|
||||
fetchRecentlyDeletedCategories()
|
||||
}
|
||||
|
||||
deinit {
|
||||
unsubscribeFromBookmarksManagerNotifications()
|
||||
}
|
||||
|
||||
// MARK: - Private methods
|
||||
private func subscribeOnBookmarksManagerNotifications() {
|
||||
recentlyDeletedCategoriesManager.add(self)
|
||||
}
|
||||
|
||||
private func unsubscribeFromBookmarksManagerNotifications() {
|
||||
recentlyDeletedCategoriesManager.remove(self)
|
||||
}
|
||||
|
||||
private func updateState(to newState: State) {
|
||||
guard state != newState else { return }
|
||||
state = newState
|
||||
stateDidChange?(state)
|
||||
}
|
||||
|
||||
private func updateFilteredDataSource(_ dataSource: [Section.Model]) {
|
||||
filteredDataSource = dataSource.filtered(using: searchText)
|
||||
filteredDataSourceDidChange?(filteredDataSource)
|
||||
}
|
||||
|
||||
private func updateSelectionAtIndexPath(_ indexPath: IndexPath, isSelected: Bool) {
|
||||
if isSelected {
|
||||
updateState(to: .someSelected)
|
||||
} else {
|
||||
let allDeselected = dataSource.allSatisfy { $0.content.isEmpty }
|
||||
updateState(to: allDeselected ? .nothingSelected : .someSelected)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeCategories(at indexPaths: [IndexPath], completion: ([URL]) -> Void) {
|
||||
var fileToRemoveURLs: [URL]
|
||||
if indexPaths.isEmpty {
|
||||
// Remove all without selection.
|
||||
fileToRemoveURLs = dataSource.flatMap { $0.content.map { $0.fileURL } }
|
||||
dataSource.removeAll()
|
||||
} else {
|
||||
fileToRemoveURLs = [URL]()
|
||||
indexPaths.forEach { [weak self] indexPath in
|
||||
guard let self else { return }
|
||||
let fileToRemoveURL = self.filteredDataSource[indexPath.section].content[indexPath.row].fileURL
|
||||
self.dataSource[indexPath.section].content.removeAll { $0.fileURL == fileToRemoveURL }
|
||||
fileToRemoveURLs.append(fileToRemoveURL)
|
||||
}
|
||||
}
|
||||
updateFilteredDataSource(dataSource)
|
||||
updateState(to: .nothingSelected)
|
||||
completion(fileToRemoveURLs)
|
||||
}
|
||||
|
||||
private func removeSelectedCategories(completion: ([URL]) -> Void) {
|
||||
let removeAll = selectedIndexPaths.isEmpty || selectedIndexPaths.count == dataSource.flatMap({ $0.content }).count
|
||||
removeCategories(at: removeAll ? [] : selectedIndexPaths, completion: completion)
|
||||
selectedIndexPaths.removeAll()
|
||||
updateState(to: .nothingSelected)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public methods
|
||||
extension RecentlyDeletedCategoriesViewModel {
|
||||
func fetchRecentlyDeletedCategories() {
|
||||
let categories = recentlyDeletedCategoriesManager.getRecentlyDeletedCategories()
|
||||
guard !categories.isEmpty else { return }
|
||||
dataSource = [Section.Model(content: categories)]
|
||||
updateFilteredDataSource(dataSource)
|
||||
}
|
||||
|
||||
func deleteCategory(at indexPath: IndexPath) {
|
||||
removeCategories(at: [indexPath]) { recentlyDeletedCategoriesManager.deleteRecentlyDeletedCategory(at: $0) }
|
||||
}
|
||||
|
||||
func deleteSelectedCategories() {
|
||||
removeSelectedCategories { recentlyDeletedCategoriesManager.deleteRecentlyDeletedCategory(at: $0) }
|
||||
}
|
||||
|
||||
func recoverCategory(at indexPath: IndexPath) {
|
||||
removeCategories(at: [indexPath]) { recentlyDeletedCategoriesManager.recoverRecentlyDeletedCategories(at: $0) }
|
||||
}
|
||||
|
||||
func recoverSelectedCategories() {
|
||||
removeSelectedCategories { recentlyDeletedCategoriesManager.recoverRecentlyDeletedCategories(at: $0) }
|
||||
}
|
||||
|
||||
func startSelecting() {
|
||||
updateState(to: .nothingSelected)
|
||||
}
|
||||
|
||||
func selectCategory(at indexPath: IndexPath) {
|
||||
selectedIndexPaths.append(indexPath)
|
||||
updateState(to: .someSelected)
|
||||
}
|
||||
|
||||
func deselectCategory(at indexPath: IndexPath) {
|
||||
selectedIndexPaths.removeAll { $0 == indexPath }
|
||||
if selectedIndexPaths.isEmpty {
|
||||
updateState(to: state == .searching ? .searching : .nothingSelected)
|
||||
}
|
||||
}
|
||||
|
||||
func selectAllCategories() {
|
||||
selectedIndexPaths = dataSource.enumerated().flatMap { sectionIndex, section in
|
||||
section.content.indices.map { IndexPath(row: $0, section: sectionIndex) }
|
||||
}
|
||||
updateState(to: .someSelected)
|
||||
}
|
||||
|
||||
func deselectAllCategories() {
|
||||
selectedIndexPaths.removeAll()
|
||||
updateState(to: state == .searching ? .searching : .nothingSelected)
|
||||
}
|
||||
|
||||
func cancelSelecting() {
|
||||
selectedIndexPaths.removeAll()
|
||||
updateState(to: .nothingSelected)
|
||||
}
|
||||
|
||||
func startSearching() {
|
||||
updateState(to: .searching)
|
||||
}
|
||||
|
||||
func cancelSearching() {
|
||||
searchText.removeAll()
|
||||
selectedIndexPaths.removeAll()
|
||||
updateFilteredDataSource(dataSource)
|
||||
updateState(to: .nothingSelected)
|
||||
}
|
||||
|
||||
func search(_ searchText: String) {
|
||||
updateState(to: .searching)
|
||||
guard !searchText.isEmpty else {
|
||||
cancelSearching()
|
||||
return
|
||||
}
|
||||
self.searchText = searchText
|
||||
updateFilteredDataSource(dataSource)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BookmarksObserver
|
||||
|
||||
extension RecentlyDeletedCategoriesViewModel: BookmarksObserver {
|
||||
func onBookmarksLoadFinished() {
|
||||
fetchRecentlyDeletedCategories()
|
||||
}
|
||||
|
||||
func onRecentlyDeletedBookmarksCategoriesChanged() {
|
||||
fetchRecentlyDeletedCategories()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension Array where Element == RecentlyDeletedCategoriesViewModel.Section.Model {
|
||||
func filtered(using searchText: String) -> [Element] {
|
||||
let filteredArray = map { section in
|
||||
let filteredContent = section.content.filter {
|
||||
guard !searchText.isEmpty else { return true }
|
||||
return $0.title.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
return RecentlyDeletedCategoriesViewModel.Section.Model(content: filteredContent)
|
||||
}
|
||||
return filteredArray
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
final class RecentlyDeletedTableViewCell: UITableViewCell {
|
||||
|
||||
struct ViewModel: Equatable, Hashable {
|
||||
let fileName: String
|
||||
let fileURL: URL
|
||||
let deletionDate: Date
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func configureWith(_ viewModel: ViewModel) {
|
||||
textLabel?.text = viewModel.fileName
|
||||
detailTextLabel?.text = DateTimeFormatter.dateString(from: viewModel.deletionDate,
|
||||
dateStyle: .medium,
|
||||
timeStyle: .medium)
|
||||
}
|
||||
}
|
||||
|
||||
extension RecentlyDeletedTableViewCell.ViewModel {
|
||||
init(_ category: RecentlyDeletedCategory) {
|
||||
self.fileName = category.title
|
||||
self.fileURL = category.fileURL
|
||||
self.deletionDate = category.deletionDate
|
||||
}
|
||||
}
|
||||
81
iphone/Maps/Bridging-Header.h
Normal file
81
iphone/Maps/Bridging-Header.h
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "AuthenticationServices/AuthenticationServices.h"
|
||||
#import "CarPlay/CarPlay.h"
|
||||
#import "UIKit/UIKit.h"
|
||||
|
||||
#import "private.h"
|
||||
|
||||
#import <CoreApi/CoreApi.h>
|
||||
|
||||
#import "DeepLinkRouteStrategyAdapter.h"
|
||||
#import "EAGLView.h"
|
||||
#import "FirstSession.h"
|
||||
#import "MWMActionBarButton.h"
|
||||
#import "MWMActivityViewController.h"
|
||||
#import "MWMAlertViewController.h"
|
||||
#import "MWMAvailableAreaAffectDirection.h"
|
||||
#import "MWMBottomMenuState.h"
|
||||
#import "MWMButtonCell.h"
|
||||
#import "MWMCarPlaySearchResultObject.h"
|
||||
#import "MWMCarPlaySearchService.h"
|
||||
#import "MWMCircularProgress.h"
|
||||
#import "MWMCollectionViewController.h"
|
||||
#import "MWMConsts.h"
|
||||
#import "MWMController.h"
|
||||
#import "MWMEditorHelper.h"
|
||||
#import "MWMFrameworkListener.h"
|
||||
#import "MWMKeyboard.h"
|
||||
#import "MWMLocationManager.h"
|
||||
#import "MWMLocationModeListener.h"
|
||||
#import "MWMMailViewController.h"
|
||||
#import "MWMMapDownloaderButtonTableViewCell.h"
|
||||
#import "MWMMapDownloaderCellHeader.h"
|
||||
#import "MWMMapDownloaderLargeCountryTableViewCell.h"
|
||||
#import "MWMMapDownloaderPlaceTableViewCell.h"
|
||||
#import "MWMMapDownloaderSubplaceTableViewCell.h"
|
||||
#import "MWMMapDownloaderTableViewCell.h"
|
||||
#import "MWMMapViewControlsManager.h"
|
||||
#import "MWMMapWidgetsHelper.h"
|
||||
#import "MWMNavigationController.h"
|
||||
#import "MWMNavigationDashboardEntity.h"
|
||||
#import "MWMNavigationDashboardManager.h"
|
||||
#import "MWMNetworkPolicy+UI.h"
|
||||
#import "MWMNoMapsViewController.h"
|
||||
#import "MWMNoteCell.h"
|
||||
#import "MWMPlacePageManagerHelper.h"
|
||||
#import "MWMRouteManagerPointType.h"
|
||||
#import "MWMRouter.h"
|
||||
#import "MWMRouterResultCode.h"
|
||||
#import "MWMRouterTransitStepInfo.h"
|
||||
#import "MWMRoutingManager.h"
|
||||
#import "MWMRoutingOptions.h"
|
||||
#import "MWMSearchNoResults.h"
|
||||
#import "MWMSettings.h"
|
||||
#import "MWMSideButtons.h"
|
||||
#import "MWMSpeedCameraManagerMode.h"
|
||||
#import "MWMStorage+UI.h"
|
||||
#import "MWMTableViewCell.h"
|
||||
#import "MWMTableViewController.h"
|
||||
#import "MWMTextToSpeech.h"
|
||||
#import "MWMTextToSpeechObserver.h"
|
||||
#import "MWMTextView.h"
|
||||
#import "MWMTrafficButtonViewController.h"
|
||||
#import "MWMViewController.h"
|
||||
#import "MapViewController.h"
|
||||
#import "MapsAppDelegate.h"
|
||||
#import "SwizzleStyle.h"
|
||||
#import "UIButton+RuntimeAttributes.h"
|
||||
#import "UIColor+MapsMeColor.h"
|
||||
#import "UIFont+MapsMeFonts.h"
|
||||
#import "UIImageView+WebImage.h"
|
||||
#import "UIKitCategories.h"
|
||||
#import "UIViewController+Navigation.h"
|
||||
#import "WebViewController.h"
|
||||
#import "MWMMapViewControlsCommon.h"
|
||||
#import "MWMSearchCommonCell.h"
|
||||
#import "MWMSearchSuggestionCell.h"
|
||||
#import "MWMSearch.h"
|
||||
#import "SearchResult.h"
|
||||
47
iphone/Maps/Bridging/BridgeControllers.swift
Normal file
47
iphone/Maps/Bridging/BridgeControllers.swift
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
/// Class for accesing SwiftUI views from Objective-C code
|
||||
@objc class BridgeControllers: NSObject {
|
||||
/// The `ProfileView` for presentation in an alert
|
||||
@objc static func profileAsAlert() -> UIViewController {
|
||||
let profileBridgeController = UIHostingController(rootView: ProfileView(isPresentedAsAlert: true))
|
||||
profileBridgeController.view.backgroundColor = .systemGroupedBackground
|
||||
return profileBridgeController
|
||||
}
|
||||
|
||||
/// The `RoutingOptionsView` for presentation in an alert
|
||||
@objc static func routingOptions() -> UIViewController {
|
||||
let routinOptionsBridgeController = UIHostingController(rootView: RoutingOptionsView())
|
||||
routinOptionsBridgeController.view.backgroundColor = .systemGroupedBackground
|
||||
return routinOptionsBridgeController
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Class for using the SwiftUI `AboutView` in the interface builder
|
||||
class AboutBridgeController: UIHostingController<AboutView> {
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder, rootView: AboutView())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Class for using the SwiftUI `SettingsView` in the interface builder
|
||||
class SettingsBridgeController: UIHostingController<SettingsView> {
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder, rootView: SettingsView())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Class for using the SwiftUI `ProfileView` in the interface builder
|
||||
class ProfileBridgeController: UIHostingController<ProfileView> {
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder, rootView: ProfileView())
|
||||
self.view.tintColor = .toolbarAccent
|
||||
}
|
||||
}
|
||||
42
iphone/Maps/Bridging/EmbeddedSafariView.swift
Normal file
42
iphone/Maps/Bridging/EmbeddedSafariView.swift
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import SwiftUI
|
||||
|
||||
/// View for Safari via a WebKit view
|
||||
struct EmbeddedSafariView: View {
|
||||
// MARK: Properties
|
||||
|
||||
/// If the content is loading
|
||||
@State private var isLoading: Bool = true
|
||||
|
||||
|
||||
/// The view height
|
||||
@State private var height: CGFloat = .zero
|
||||
|
||||
|
||||
/// The url
|
||||
let url: URL
|
||||
|
||||
|
||||
/// If the view should resize itself to the height of the website content
|
||||
var hasDynamicHeight: Bool = true
|
||||
|
||||
|
||||
/// The actual view
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if hasDynamicHeight {
|
||||
EmbeddedSafariViewContent(isLoading: $isLoading, height: $height, hasDynamicHeight: hasDynamicHeight, url: url)
|
||||
.frame(height: .infinity)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
} else {
|
||||
EmbeddedSafariViewContent(isLoading: $isLoading, height: $height, hasDynamicHeight: hasDynamicHeight, url: url)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
|
||||
if isLoading {
|
||||
ProgressView()
|
||||
.controlSize(.large)
|
||||
.frame(minHeight: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
iphone/Maps/Bridging/EmbeddedSafariViewContent.swift
Normal file
59
iphone/Maps/Bridging/EmbeddedSafariViewContent.swift
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import SwiftUI
|
||||
import WebKit
|
||||
|
||||
/// Content of the view for Safari via a WebKit view
|
||||
struct EmbeddedSafariViewContent: UIViewRepresentable {
|
||||
// MARK: Properties
|
||||
|
||||
/// If the content is loading
|
||||
@Binding var isLoading: Bool
|
||||
|
||||
|
||||
/// The view height
|
||||
@Binding var height: CGFloat
|
||||
|
||||
|
||||
/// If the view should resize itself to the height of the website content
|
||||
var hasDynamicHeight: Bool = true
|
||||
|
||||
|
||||
/// The url
|
||||
let url: URL
|
||||
|
||||
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Create a coodindator for the WebKit view
|
||||
func makeCoordinator() -> EmbeddedSafariViewCoordinator {
|
||||
EmbeddedSafariViewCoordinator(self)
|
||||
}
|
||||
|
||||
|
||||
/// Create a WebKit view
|
||||
/// - Parameter context: The context
|
||||
/// - Returns: The WebKit view
|
||||
func makeUIView(context: UIViewRepresentableContext<EmbeddedSafariViewContent>) -> WKWebView {
|
||||
let uiView = WKWebView()
|
||||
uiView.navigationDelegate = context.coordinator
|
||||
uiView.scrollView.isScrollEnabled = !hasDynamicHeight
|
||||
uiView.scrollView.showsHorizontalScrollIndicator = false
|
||||
uiView.allowsBackForwardNavigationGestures = false
|
||||
uiView.allowsLinkPreview = false
|
||||
if #available(iOS 16.0, *) {
|
||||
uiView.isFindInteractionEnabled = false
|
||||
}
|
||||
uiView.isOpaque = false
|
||||
uiView.backgroundColor = .clear
|
||||
uiView.underPageBackgroundColor = .clear
|
||||
uiView.load(URLRequest(url: url))
|
||||
return uiView
|
||||
}
|
||||
|
||||
|
||||
/// Update the WebKit view
|
||||
/// - Parameter context: The context
|
||||
func updateUIView(_ uiView: WKWebView, context: UIViewRepresentableContext<EmbeddedSafariViewContent>) {
|
||||
uiView.load(URLRequest(url: url))
|
||||
}
|
||||
}
|
||||
57
iphone/Maps/Bridging/EmbeddedSafariViewCoordinator.swift
Normal file
57
iphone/Maps/Bridging/EmbeddedSafariViewCoordinator.swift
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import WebKit
|
||||
|
||||
/// Coordinator of the view for Safari via a WebKit view
|
||||
class EmbeddedSafariViewCoordinator: NSObject {
|
||||
// MARK: Properties
|
||||
|
||||
/// The content
|
||||
var content: EmbeddedSafariViewContent
|
||||
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initalize the coordinator with the matching content
|
||||
/// - Parameter content: The content
|
||||
init(_ content: EmbeddedSafariViewContent) {
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - `WKNavigationDelegate`
|
||||
extension EmbeddedSafariViewCoordinator: WKNavigationDelegate {
|
||||
// MARK: Methods
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
if webView.isLoading == false {
|
||||
self.content.isLoading = false
|
||||
|
||||
if content.hasDynamicHeight {
|
||||
webView.evaluateJavaScript(
|
||||
"document.body.scrollHeight",
|
||||
completionHandler: { (result, error) in
|
||||
if let height = result as? CGFloat {
|
||||
self.content.height = height
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) {
|
||||
if let url = navigationAction.request.url {
|
||||
if url.absoluteString.starts(with: "file:///") {
|
||||
decisionHandler(.allow)
|
||||
return
|
||||
} else if navigationAction.navigationType == .linkActivated {
|
||||
if UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decisionHandler(.cancel)
|
||||
}
|
||||
}
|
||||
44
iphone/Maps/Bridging/SafariView.swift
Normal file
44
iphone/Maps/Bridging/SafariView.swift
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import SafariServices
|
||||
import SwiftUI
|
||||
|
||||
/// View for Safari via a Safari view controller
|
||||
struct SafariView: UIViewControllerRepresentable {
|
||||
// MARK: Properties
|
||||
|
||||
/// The notification name for dismissing this view
|
||||
static let dismissNotificationName: Notification.Name = Notification.Name(rawValue: "DismissSafariView")
|
||||
|
||||
|
||||
/// The url
|
||||
let url: URL
|
||||
|
||||
|
||||
/// The type of dismiss button
|
||||
var dismissButton: SFSafariViewController.DismissButtonStyle = .done
|
||||
|
||||
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Create a Safari view controller
|
||||
/// - Parameter context: The context
|
||||
/// - Returns: The Safari view controller
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<SafariView>) -> SFSafariViewController {
|
||||
let safariViewControllerConfiguration = SFSafariViewController.Configuration()
|
||||
safariViewControllerConfiguration.activityButton = nil
|
||||
safariViewControllerConfiguration.barCollapsingEnabled = true
|
||||
|
||||
let safariViewController = SFSafariViewController(url: url, configuration: safariViewControllerConfiguration)
|
||||
safariViewController.preferredBarTintColor = UIColor.accent
|
||||
safariViewController.preferredControlTintColor = UIColor.white
|
||||
safariViewController.dismissButtonStyle = dismissButton
|
||||
return safariViewController
|
||||
}
|
||||
|
||||
|
||||
/// Update the Safari view controller
|
||||
/// - Parameter context: The context
|
||||
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<SafariView>) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
14
iphone/Maps/Categories/Bundle+Init.swift
Normal file
14
iphone/Maps/Categories/Bundle+Init.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
extension Bundle {
|
||||
func load<T:UIView>(viewClass: T.Type, owner: Any? = nil, options: [AnyHashable: Any]? = nil) -> T? {
|
||||
return loadNibNamed(String(describing: viewClass), owner: owner, options: options as? [UINib.OptionsKey : Any])?.first as? T
|
||||
}
|
||||
|
||||
@objc func load(viewClass: AnyClass, owner: Any? = nil, options: [AnyHashable: Any]? = nil) -> [Any]? {
|
||||
return loadNibNamed(toString(viewClass), owner: owner, options: options as? [UINib.OptionsKey : Any])
|
||||
}
|
||||
|
||||
@objc func load(plist: String) -> Dictionary<String, AnyObject>? {
|
||||
guard let path = Bundle.main.path(forResource: plist, ofType: "plist") else { return nil }
|
||||
return NSDictionary(contentsOfFile: path) as? [String: AnyObject]
|
||||
}
|
||||
}
|
||||
16
iphone/Maps/Categories/CALayer+SetCorner.swift
Normal file
16
iphone/Maps/Categories/CALayer+SetCorner.swift
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
extension CALayer {
|
||||
func setCornerRadius(_ cornerRadius: CornerRadius,
|
||||
maskedCorners: CACornerMask? = nil) {
|
||||
self.cornerRadius = cornerRadius.value
|
||||
if let maskedCorners {
|
||||
self.maskedCorners = maskedCorners
|
||||
}
|
||||
cornerCurve = .continuous
|
||||
}
|
||||
}
|
||||
|
||||
extension CACornerMask {
|
||||
static var all: CACornerMask {
|
||||
return [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
}
|
||||
}
|
||||
7
iphone/Maps/Categories/CLLocation+Mercator.h
Normal file
7
iphone/Maps/Categories/CLLocation+Mercator.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#include "geometry/point2d.hpp"
|
||||
|
||||
@interface CLLocation (Mercator)
|
||||
|
||||
- (m2::PointD)mercator;
|
||||
|
||||
@end
|
||||
8
iphone/Maps/Categories/CLLocation+Mercator.mm
Normal file
8
iphone/Maps/Categories/CLLocation+Mercator.mm
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#import "CLLocation+Mercator.h"
|
||||
#import "MWMLocationHelpers.h"
|
||||
|
||||
@implementation CLLocation (Mercator)
|
||||
|
||||
- (m2::PointD)mercator { return location_helpers::ToMercator(self.coordinate); }
|
||||
|
||||
@end
|
||||
19
iphone/Maps/Categories/MWMCategory+PlacesCountTitle.swift
Normal file
19
iphone/Maps/Categories/MWMCategory+PlacesCountTitle.swift
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import Foundation
|
||||
|
||||
extension BookmarkGroup {
|
||||
@objc func placesCountTitle() -> String {
|
||||
let bookmarks = String(format: L("bookmarks_places"), bookmarksCount)
|
||||
let tracks = String(format: L("tracks"), trackCount)
|
||||
|
||||
if (bookmarksCount == 0 && trackCount == 0) || (bookmarksCount > 0 && trackCount > 0) {
|
||||
return "\(bookmarks), \(tracks)"
|
||||
}
|
||||
|
||||
if (bookmarksCount > 0) {
|
||||
return bookmarks
|
||||
}
|
||||
|
||||
return tracks
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#include "geometry/point2d.hpp"
|
||||
|
||||
@interface MWMMapViewControlsManager (AddPlace)
|
||||
|
||||
- (void)addPlace:(BOOL)isBusiness position:(nullable m2::PointD const *)optionalPosition;
|
||||
|
||||
@end
|
||||
100
iphone/Maps/Categories/NSAttributedString+HTML.swift
Normal file
100
iphone/Maps/Categories/NSAttributedString+HTML.swift
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
extension NSAttributedString {
|
||||
@objc
|
||||
public class func string(withHtml htmlString:String, defaultAttributes attributes:[NSAttributedString.Key : Any]) -> NSAttributedString? {
|
||||
guard let data = htmlString.data(using: .utf8) else { return nil }
|
||||
guard let text = try? NSMutableAttributedString(data: data,
|
||||
options: [.documentType: NSAttributedString.DocumentType.html,
|
||||
.characterEncoding: String.Encoding.utf8.rawValue],
|
||||
documentAttributes: nil) else { return nil }
|
||||
text.addAttributes(attributes, range: NSMakeRange(0, text.length))
|
||||
return text
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
@objc convenience init?(htmlString: String, baseFont: UIFont, paragraphStyle: NSParagraphStyle?, estimatedWidth: CGFloat = 0) {
|
||||
self.init(htmlString: htmlString, baseFont: baseFont)
|
||||
if let paragraphStyle = paragraphStyle {
|
||||
addAttribute(.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, length))
|
||||
}
|
||||
|
||||
guard estimatedWidth > 0 else { return }
|
||||
enumerateAttachments(estimatedWidth: estimatedWidth)
|
||||
}
|
||||
|
||||
@objc convenience init?(htmlString: String, baseFont: UIFont) {
|
||||
guard let data = htmlString.data(using: .utf8) else { return nil }
|
||||
|
||||
do {
|
||||
try self.init(data: data,
|
||||
options: [.documentType : NSAttributedString.DocumentType.html,
|
||||
.characterEncoding: String.Encoding.utf8.rawValue],
|
||||
documentAttributes: nil)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
|
||||
enumerateFont(baseFont)
|
||||
}
|
||||
|
||||
@objc convenience init?(htmlString: String, baseFont: UIFont, estimatedWidth: CGFloat) {
|
||||
guard let data = htmlString.data(using: .utf8) else { return nil }
|
||||
|
||||
do {
|
||||
try self.init(data: data,
|
||||
options: [.documentType : NSAttributedString.DocumentType.html,
|
||||
.characterEncoding: String.Encoding.utf8.rawValue],
|
||||
documentAttributes: nil)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
|
||||
enumerateFont(baseFont)
|
||||
|
||||
guard estimatedWidth > 0 else { return }
|
||||
|
||||
enumerateAttachments(estimatedWidth: estimatedWidth)
|
||||
}
|
||||
|
||||
func enumerateFont(_ baseFont: UIFont) {
|
||||
enumerateAttribute(.font, in: NSMakeRange(0, length), options: []) { (value, range, _) in
|
||||
if let font = value as? UIFont,
|
||||
let descriptor = baseFont.fontDescriptor.withSymbolicTraits(font.fontDescriptor.symbolicTraits) {
|
||||
let newFont = UIFont(descriptor: descriptor, size: baseFont.pointSize)
|
||||
addAttribute(.font, value: newFont, range: range)
|
||||
} else {
|
||||
addAttribute(.font, value: baseFont, range: range)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func enumerateAttachments(estimatedWidth: CGFloat) {
|
||||
enumerateAttribute(.attachment, in: NSMakeRange(0, length), options: []) { (value, range, _) in
|
||||
if let attachement = value as? NSTextAttachment,
|
||||
let image = attachement.image(forBounds: attachement.bounds, textContainer: NSTextContainer(), characterIndex: range.location) {
|
||||
if image.size.width > estimatedWidth || attachement.bounds.width > estimatedWidth {
|
||||
let resizedAttachment = NSTextAttachment()
|
||||
if image.size.width > estimatedWidth {
|
||||
let newImage = resizeImage(image: image, scale: estimatedWidth/image.size.width) ?? image
|
||||
resizedAttachment.image = newImage
|
||||
} else {
|
||||
resizedAttachment.image = image
|
||||
}
|
||||
addAttribute(.attachment, value: resizedAttachment, range: range)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resizeImage(image: UIImage, scale: CGFloat) -> UIImage? {
|
||||
let newSize = CGSize(width: image.size.width*scale, height: image.size.height*scale)
|
||||
let rect = CGRect(origin: CGPoint.zero, size: newSize)
|
||||
|
||||
let renderer = UIGraphicsImageRenderer(size: newSize)
|
||||
let newImage = renderer.image { context in
|
||||
image.draw(in: rect)
|
||||
}
|
||||
return newImage
|
||||
}
|
||||
}
|
||||
11
iphone/Maps/Categories/NSDate+TimeDistance.h
Normal file
11
iphone/Maps/Categories/NSDate+TimeDistance.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSDate (TimeDistance)
|
||||
|
||||
- (NSInteger)daysToNow;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
14
iphone/Maps/Categories/NSDate+TimeDistance.m
Normal file
14
iphone/Maps/Categories/NSDate+TimeDistance.m
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#import "NSDate+TimeDistance.h"
|
||||
|
||||
@implementation NSDate (TimeDistance)
|
||||
|
||||
- (NSInteger)daysToNow {
|
||||
NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitDay
|
||||
fromDate:self
|
||||
toDate:[NSDate date]
|
||||
options:NSCalendarWrapComponents];
|
||||
|
||||
return components.day;
|
||||
}
|
||||
|
||||
@end
|
||||
11
iphone/Maps/Categories/NSString+Categories.h
Normal file
11
iphone/Maps/Categories/NSString+Categories.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
@interface NSString (MapsMeSize)
|
||||
|
||||
- (CGSize)sizeWithDrawSize:(CGSize)size font:(UIFont *)font;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSString (MapsMeRanges)
|
||||
|
||||
- (NSArray<NSValue *> *)rangesOfString:(NSString *)aString;
|
||||
|
||||
@end
|
||||
36
iphone/Maps/Categories/NSString+Categories.m
Normal file
36
iphone/Maps/Categories/NSString+Categories.m
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#import "NSString+Categories.h"
|
||||
|
||||
@implementation NSString (MapsMeSize)
|
||||
|
||||
- (CGSize)sizeWithDrawSize:(CGSize)drawSize font:(UIFont *)font {
|
||||
CGRect rect = [self boundingRectWithSize:drawSize
|
||||
options:NSStringDrawingUsesLineFragmentOrigin
|
||||
attributes:@{NSFontAttributeName : font}
|
||||
context:nil];
|
||||
return CGRectIntegral(rect).size;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSString (MapsMeRanges)
|
||||
|
||||
- (NSArray<NSValue *> *)rangesOfString:(NSString *)aString {
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
if (self.length == 0) {
|
||||
return [result copy];
|
||||
}
|
||||
|
||||
NSRange searchRange = NSMakeRange(0, self.length);
|
||||
while (searchRange.location < self.length) {
|
||||
searchRange.length = self.length - searchRange.location;
|
||||
NSRange foundRange = [self rangeOfString:aString options:NSCaseInsensitiveSearch range:searchRange];
|
||||
if (foundRange.location == NSNotFound) {
|
||||
break;
|
||||
}
|
||||
searchRange.location = foundRange.location + foundRange.length;
|
||||
[result addObject:[NSValue valueWithRange:foundRange]];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
29
iphone/Maps/Categories/String+BoundingRect.swift
Normal file
29
iphone/Maps/Categories/String+BoundingRect.swift
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
extension String {
|
||||
func size(width: CGFloat, font: UIFont, maxNumberOfLines: Int = 0) -> CGSize {
|
||||
if isEmpty {
|
||||
return CGSize.zero
|
||||
}
|
||||
let lineHeight = font.lineHeight
|
||||
let maximumHeight = maxNumberOfLines == 0 ? CGFloat.greatestFiniteMagnitude : lineHeight * CGFloat(maxNumberOfLines + 1)
|
||||
let constraintSize = CGSize(width: width, height: maximumHeight)
|
||||
let options: NSStringDrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading, .truncatesLastVisibleLine]
|
||||
let paragraph = NSMutableParagraphStyle()
|
||||
paragraph.lineBreakMode = .byWordWrapping
|
||||
paragraph.alignment = .natural
|
||||
let attributes = [
|
||||
NSAttributedString.Key.font: font,
|
||||
NSAttributedString.Key.paragraphStyle: paragraph,
|
||||
]
|
||||
var rect = (self as NSString).boundingRect(with: constraintSize, options: options, attributes: attributes, context: nil)
|
||||
var numberOfLines = ceil(rect.height / lineHeight)
|
||||
if maxNumberOfLines != 0 {
|
||||
if width - rect.width < font.ascender {
|
||||
rect.size.width = width - font.ascender
|
||||
numberOfLines += 1
|
||||
}
|
||||
numberOfLines = min(numberOfLines, CGFloat(maxNumberOfLines))
|
||||
}
|
||||
return CGSize(width: ceil(rect.width), height: ceil(numberOfLines * lineHeight))
|
||||
}
|
||||
}
|
||||
28
iphone/Maps/Categories/UIApplication+LoadingOverlay.swift
Normal file
28
iphone/Maps/Categories/UIApplication+LoadingOverlay.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
extension UIApplication {
|
||||
private static let overlayViewController = LoadingOverlayViewController()
|
||||
|
||||
@objc
|
||||
func showLoadingOverlay(completion: (() -> Void)? = nil) {
|
||||
guard let window = (self.connectedScenes.filter { $0.activationState == .foregroundActive }.first(where: { $0 is UIWindowScene }) as? UIWindowScene)?.windows.first(where: { $0.isKeyWindow }) else {
|
||||
completion?()
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.overlayViewController.modalPresentationStyle = .overFullScreen
|
||||
UIApplication.overlayViewController.modalTransitionStyle = .crossDissolve
|
||||
if window.rootViewController?.presentedViewController != nil {
|
||||
window.rootViewController?.presentedViewController?.present(UIApplication.overlayViewController, animated: true, completion: completion)
|
||||
} else {
|
||||
window.rootViewController?.present(UIApplication.overlayViewController, animated: true, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func hideLoadingOverlay(completion: (() -> Void)? = nil) {
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.overlayViewController.dismiss(animated: true, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
12
iphone/Maps/Categories/UIButton+ImagePadding.swift
Normal file
12
iphone/Maps/Categories/UIButton+ImagePadding.swift
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
extension UIButton {
|
||||
@objc func setImagePadding(_ padding: CGFloat) {
|
||||
let isRightToLeft = UIView.userInterfaceLayoutDirection(for: self.semanticContentAttribute) == .rightToLeft
|
||||
if isRightToLeft {
|
||||
self.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: padding)
|
||||
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -2 * padding)
|
||||
} else {
|
||||
self.contentEdgeInsets = UIEdgeInsets(top: 0, left: padding, bottom: 0, right: 0)
|
||||
self.imageEdgeInsets = UIEdgeInsets(top: 0, left: -2 * padding, bottom: 0, right: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
5
iphone/Maps/Categories/UIButton+Orientation.h
Normal file
5
iphone/Maps/Categories/UIButton+Orientation.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
@interface UIButton (Orientation)
|
||||
|
||||
- (void)matchInterfaceOrientation;
|
||||
|
||||
@end
|
||||
13
iphone/Maps/Categories/UIButton+Orientation.m
Normal file
13
iphone/Maps/Categories/UIButton+Orientation.m
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#import "UIButton+Orientation.h"
|
||||
|
||||
#import <CoreApi/MWMCommon.h>
|
||||
|
||||
@implementation UIButton (Orientation)
|
||||
|
||||
- (void)matchInterfaceOrientation
|
||||
{
|
||||
if (isInterfaceRightToLeft())
|
||||
self.imageView.transform = CGAffineTransformMakeScale(-1, 1);
|
||||
}
|
||||
|
||||
@end
|
||||
7
iphone/Maps/Categories/UIButton+RuntimeAttributes.h
Normal file
7
iphone/Maps/Categories/UIButton+RuntimeAttributes.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UIButton (RuntimeAttributes)
|
||||
|
||||
@property(copy, nonatomic) NSString * localizedText;
|
||||
|
||||
@end
|
||||
18
iphone/Maps/Categories/UIButton+RuntimeAttributes.m
Normal file
18
iphone/Maps/Categories/UIButton+RuntimeAttributes.m
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#import <objc/runtime.h>
|
||||
#import "UIButton+RuntimeAttributes.h"
|
||||
|
||||
@implementation UIButton (RuntimeAttributes)
|
||||
|
||||
- (void)setLocalizedText:(NSString *)localizedText
|
||||
{
|
||||
[self setTitle:L(localizedText) forState:UIControlStateNormal];
|
||||
[self setTitle:L(localizedText) forState:UIControlStateDisabled];
|
||||
}
|
||||
|
||||
- (NSString *)localizedText
|
||||
{
|
||||
NSString * title = [self titleForState:UIControlStateNormal];
|
||||
return L(title);
|
||||
}
|
||||
|
||||
@end
|
||||
17
iphone/Maps/Categories/UICollectionView+Cells.swift
Normal file
17
iphone/Maps/Categories/UICollectionView+Cells.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
extension UICollectionView {
|
||||
@objc func register(cellClass: AnyClass) {
|
||||
register(UINib(cellClass), forCellWithReuseIdentifier: toString(cellClass))
|
||||
}
|
||||
|
||||
@objc func dequeueReusableCell(withCellClass cellClass: AnyClass, indexPath: IndexPath) -> UICollectionViewCell {
|
||||
return dequeueReusableCell(withReuseIdentifier: toString(cellClass), for: indexPath)
|
||||
}
|
||||
|
||||
func register<Cell>(cell: Cell.Type) where Cell: UICollectionViewCell {
|
||||
register(cell, forCellWithReuseIdentifier: toString(cell))
|
||||
}
|
||||
|
||||
func dequeueReusableCell<Cell>(cell: Cell.Type, indexPath: IndexPath) -> Cell where Cell: UICollectionViewCell {
|
||||
return dequeueReusableCell(withReuseIdentifier: toString(cell), for: indexPath) as! Cell
|
||||
}
|
||||
}
|
||||
42
iphone/Maps/Categories/UIColor+MapsMeColor.h
Normal file
42
iphone/Maps/Categories/UIColor+MapsMeColor.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#import "UIColor+PartnerColor.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface UIColor (MapsMeColor)
|
||||
|
||||
+ (UIColor *)black;
|
||||
+ (UIColor *)blackPrimaryText;
|
||||
+ (UIColor *)blackSecondaryText;
|
||||
+ (UIColor *)blackHintText;
|
||||
+ (UIColor *)red;
|
||||
+ (UIColor *)white;
|
||||
+ (UIColor *)primary;
|
||||
+ (UIColor *)pressBackground;
|
||||
+ (UIColor *)linkBlue;
|
||||
+ (UIColor *)linkBlueHighlighted;
|
||||
+ (UIColor *)buttonRed;
|
||||
+ (UIColor *)blackDividers;
|
||||
+ (UIColor *)whitePrimaryText;
|
||||
+ (UIColor *)whitePrimaryTextHighlighted;
|
||||
+ (UIColor *)whiteHintText;
|
||||
+ (UIColor *)buttonDisabledBlueText;
|
||||
+ (UIColor *)blackOpaque;
|
||||
+ (UIColor *)bookingBackground;
|
||||
+ (UIColor *)opentableBackground;
|
||||
+ (UIColor *)transparentGreen;
|
||||
+ (UIColor *)speedLimitRed;
|
||||
+ (UIColor *)speedLimitGreen;
|
||||
+ (UIColor *)speedLimitWhite;
|
||||
+ (UIColor *)speedLimitLightGray;
|
||||
+ (UIColor *)speedLimitDarkGray;
|
||||
+ (UIColor *)carplayPlaceholderBackground;
|
||||
|
||||
+ (UIColor *)colorWithName:(NSString *)colorName;
|
||||
+ (UIColor *)colorFromHexString:(NSString *)hexString;
|
||||
|
||||
+ (void)setNightMode:(BOOL)mode;
|
||||
+ (BOOL)isNightMode;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
179
iphone/Maps/Categories/UIColor+MapsMeColor.m
Normal file
179
iphone/Maps/Categories/UIColor+MapsMeColor.m
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
#import "UIColorRoutines.h"
|
||||
#import "UIColor+MapsMeColor.h"
|
||||
#import "SwiftBridge.h"
|
||||
|
||||
static BOOL isNightMode = NO;
|
||||
|
||||
@implementation UIColor (MapsMeColor)
|
||||
|
||||
// hex string without #
|
||||
+ (UIColor *)colorFromHexString:(NSString *)hexString
|
||||
{
|
||||
unsigned rgbValue = 0;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:hexString];
|
||||
[scanner setScanLocation:0];
|
||||
[scanner scanHexInt:&rgbValue];
|
||||
return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16)/255.0 green:((rgbValue & 0xFF00) >> 8)/255.0 blue:(rgbValue & 0xFF)/255.0 alpha:1.0];
|
||||
}
|
||||
|
||||
+ (void)setNightMode:(BOOL)mode
|
||||
{
|
||||
isNightMode = mode;
|
||||
}
|
||||
|
||||
+ (BOOL)isNightMode
|
||||
{
|
||||
return isNightMode;
|
||||
}
|
||||
|
||||
// Green color
|
||||
+ (UIColor *)primary
|
||||
{
|
||||
return StyleManager.shared.theme.colors.primary;
|
||||
}
|
||||
|
||||
// Use for opaque fullscreen
|
||||
+ (UIColor *)fadeBackground
|
||||
{
|
||||
return [UIColor colorWithWhite:0. alpha:alpha80];
|
||||
}
|
||||
|
||||
// Background color && press color
|
||||
+ (UIColor *)pressBackground
|
||||
{
|
||||
return StyleManager.shared.theme.colors.pressBackground;
|
||||
}
|
||||
// Red color (use for status closed in place page)
|
||||
+ (UIColor *)red
|
||||
{
|
||||
return StyleManager.shared.theme.colors.red;
|
||||
}
|
||||
|
||||
// Blue color (use for links and phone numbers)
|
||||
+ (UIColor *)linkBlue
|
||||
{
|
||||
return StyleManager.shared.theme.colors.linkBlue;
|
||||
}
|
||||
|
||||
+ (UIColor *)linkBlueHighlighted
|
||||
{
|
||||
return StyleManager.shared.theme.colors.linkBlueHighlighted;
|
||||
}
|
||||
|
||||
+ (UIColor *)linkBlueDark
|
||||
{
|
||||
return StyleManager.shared.theme.colors.linkBlueDark;
|
||||
}
|
||||
+ (UIColor *)buttonRed
|
||||
{
|
||||
return StyleManager.shared.theme.colors.buttonRed;
|
||||
}
|
||||
+ (UIColor *)black
|
||||
{
|
||||
return StyleManager.shared.theme.colors.black;
|
||||
}
|
||||
|
||||
+ (UIColor *)blackPrimaryText
|
||||
{
|
||||
return StyleManager.shared.theme.colors.blackPrimaryText;
|
||||
}
|
||||
|
||||
+ (UIColor *)blackSecondaryText
|
||||
{
|
||||
return StyleManager.shared.theme.colors.blackSecondaryText;
|
||||
}
|
||||
|
||||
+ (UIColor *)blackHintText
|
||||
{
|
||||
return StyleManager.shared.theme.colors.blackHintText;
|
||||
}
|
||||
|
||||
+ (UIColor *)blackDividers
|
||||
{
|
||||
return StyleManager.shared.theme.colors.blackDividers;
|
||||
}
|
||||
|
||||
+ (UIColor *)white
|
||||
{
|
||||
return StyleManager.shared.theme.colors.white;
|
||||
}
|
||||
|
||||
+ (UIColor *)whitePrimaryText
|
||||
{
|
||||
return [UIColor colorWithWhite:1. alpha:alpha87];
|
||||
}
|
||||
|
||||
+ (UIColor *)whitePrimaryTextHighlighted
|
||||
{
|
||||
// use only for highlighted colors!
|
||||
return [UIColor colorWithWhite:1. alpha:alpha30];
|
||||
}
|
||||
|
||||
+ (UIColor *)whiteHintText
|
||||
{
|
||||
return StyleManager.shared.theme.colors.whiteHintText;
|
||||
}
|
||||
|
||||
+ (UIColor *)buttonDisabledBlueText
|
||||
{
|
||||
return StyleManager.shared.theme.colors.buttonDisabledBlueText;
|
||||
}
|
||||
|
||||
+ (UIColor *)buttonHighlightedBlueText
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(3.) green:scaled(122.) blue:scaled(255.) alpha:alpha54];
|
||||
}
|
||||
|
||||
+ (UIColor *)blackOpaque
|
||||
{
|
||||
return StyleManager.shared.theme.colors.blackOpaque;
|
||||
}
|
||||
|
||||
+ (UIColor *)carplayPlaceholderBackground
|
||||
{
|
||||
return StyleManager.shared.theme.colors.carplayPlaceholderBackground;
|
||||
}
|
||||
|
||||
+ (UIColor *)bookingBackground
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(25.) green:scaled(69.) blue:scaled(125.) alpha:alpha100];
|
||||
}
|
||||
|
||||
+ (UIColor *)opentableBackground
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(218.) green:scaled(55) blue:scaled(67) alpha:alpha100];
|
||||
}
|
||||
|
||||
+ (UIColor *)transparentGreen
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(233) green:scaled(244) blue:scaled(233) alpha:alpha26];
|
||||
}
|
||||
|
||||
+ (UIColor *)colorWithName:(NSString *)colorName
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
return [[UIColor class] performSelector:NSSelectorFromString(colorName)];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
+ (UIColor *)speedLimitRed {
|
||||
return [UIColor colorWithRed:scaled(224) green:scaled(31) blue:scaled(31) alpha:alpha100];
|
||||
}
|
||||
|
||||
+ (UIColor *)speedLimitGreen {
|
||||
return [UIColor colorWithRed:scaled(1) green:scaled(104) blue:scaled(44) alpha:alpha100];
|
||||
}
|
||||
|
||||
+ (UIColor *)speedLimitWhite {
|
||||
return [UIColor colorWithRed:scaled(255) green:scaled(255) blue:scaled(255) alpha:alpha80];
|
||||
}
|
||||
|
||||
+ (UIColor *)speedLimitLightGray {
|
||||
return [UIColor colorWithRed:scaled(0) green:scaled(0) blue:scaled(0) alpha:alpha20];
|
||||
}
|
||||
|
||||
+ (UIColor *)speedLimitDarkGray {
|
||||
return [UIColor colorWithRed:scaled(51) green:scaled(51) blue:scaled(50) alpha:alpha100];
|
||||
}
|
||||
@end
|
||||
63
iphone/Maps/Categories/UIColor+Modifications.swift
Normal file
63
iphone/Maps/Categories/UIColor+Modifications.swift
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
extension UIColor {
|
||||
func blending(with color: UIColor) -> UIColor {
|
||||
var bgR: CGFloat = 0
|
||||
var bgG: CGFloat = 0
|
||||
var bgB: CGFloat = 0
|
||||
var bgA: CGFloat = 0
|
||||
|
||||
var fgR: CGFloat = 0
|
||||
var fgG: CGFloat = 0
|
||||
var fgB: CGFloat = 0
|
||||
var fgA: CGFloat = 0
|
||||
|
||||
self.getRed(&bgR, green: &bgG, blue: &bgB, alpha: &bgA)
|
||||
color.getRed(&fgR, green: &fgG, blue: &fgB, alpha: &fgA)
|
||||
|
||||
let r = fgA * fgR + (1 - fgA) * bgR
|
||||
let g = fgA * fgG + (1 - fgA) * bgG
|
||||
let b = fgA * fgB + (1 - fgA) * bgB
|
||||
|
||||
return UIColor(red: r, green: g, blue: b, alpha: bgA)
|
||||
}
|
||||
|
||||
func lighter(percent: CGFloat) -> UIColor {
|
||||
return colorWithBrightnessFactor(factor: 1 + percent)
|
||||
}
|
||||
|
||||
func darker(percent: CGFloat) -> UIColor {
|
||||
return colorWithBrightnessFactor(factor: 1 - percent)
|
||||
}
|
||||
|
||||
private func colorWithBrightnessFactor(factor: CGFloat) -> UIColor {
|
||||
var hue: CGFloat = 0
|
||||
var saturation: CGFloat = 0
|
||||
var brightness: CGFloat = 0
|
||||
var alpha: CGFloat = 0
|
||||
|
||||
if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
|
||||
return UIColor(hue: hue, saturation: saturation, brightness: brightness * factor, alpha: alpha)
|
||||
} else {
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
func components() -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)? {
|
||||
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||
return getRed(&r, green: &g, blue: &b, alpha: &a) ? (r,g,b,a) : nil
|
||||
}
|
||||
|
||||
static func intermediateColor( color1: UIColor, color2: UIColor, _ scale: CGFloat) -> UIColor? {
|
||||
guard let comp1 = color1.components(),
|
||||
let comp2 = color2.components() else {
|
||||
return nil
|
||||
}
|
||||
let scale = min(1, max(0, scale))
|
||||
let r = comp1.red + (comp2.red - comp1.red) * scale
|
||||
let g = comp1.green + (comp2.green - comp1.green) * scale
|
||||
let b = comp1.blue + (comp2.blue - comp1.blue) * scale
|
||||
let a = comp1.alpha + (comp2.alpha - comp1.alpha) * scale
|
||||
return UIColor(red: r, green: g, blue: b, alpha: a)
|
||||
}
|
||||
}
|
||||
18
iphone/Maps/Categories/UIColor+PartnerColor.h
Normal file
18
iphone/Maps/Categories/UIColor+PartnerColor.h
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// This file is autogenerated
|
||||
@interface UIColor (PartnerColor)
|
||||
|
||||
+ (UIColor *)partner1Background;
|
||||
+ (UIColor *)partner1TextColor;
|
||||
+ (UIColor *)partner2Background;
|
||||
+ (UIColor *)partner2TextColor;
|
||||
+ (UIColor *)partner3Background;
|
||||
+ (UIColor *)partner3TextColor;
|
||||
+ (UIColor *)partner18Background;
|
||||
+ (UIColor *)partner18TextColor;
|
||||
+ (UIColor *)partner19Background;
|
||||
+ (UIColor *)partner19TextColor;
|
||||
+ (UIColor *)partner20Background;
|
||||
+ (UIColor *)partner20TextColor;
|
||||
|
||||
@end
|
||||
|
||||
56
iphone/Maps/Categories/UIColor+PartnerColor.m
Normal file
56
iphone/Maps/Categories/UIColor+PartnerColor.m
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// This file is autogenerated
|
||||
#import "UIColorRoutines.h"
|
||||
#import "UIColor+PartnerColor.h"
|
||||
|
||||
@implementation UIColor (PartnerColor)
|
||||
+ (UIColor *)partner1Background
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(48) green:scaled(52) blue:scaled(56) alpha:1];
|
||||
}
|
||||
+ (UIColor *)partner1TextColor
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(255) green:scaled(255) blue:scaled(255) alpha:1];
|
||||
}
|
||||
+ (UIColor *)partner2Background
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(215) green:scaled(215) blue:scaled(215) alpha:1];
|
||||
}
|
||||
+ (UIColor *)partner2TextColor
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(33) green:scaled(33) blue:scaled(33) alpha:1];
|
||||
}
|
||||
+ (UIColor *)partner3Background
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(230) green:scaled(23) blue:scaled(23) alpha:1];
|
||||
}
|
||||
+ (UIColor *)partner3TextColor
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(255) green:scaled(255) blue:scaled(255) alpha:1];
|
||||
}
|
||||
+ (UIColor *)partner18Background
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(0) green:scaled(185) blue:scaled(86) alpha:100];
|
||||
}
|
||||
+ (UIColor *)partner18TextColor
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(255) green:scaled(255) blue:scaled(255) alpha:100];
|
||||
}
|
||||
+ (UIColor *)partner19Background
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(87) green:scaled(26) blue:scaled(140) alpha:100];
|
||||
}
|
||||
+ (UIColor *)partner19TextColor
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(255) green:scaled(255) blue:scaled(255) alpha:100];
|
||||
}
|
||||
+ (UIColor *)partner20Background
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(87) green:scaled(26) blue:scaled(140) alpha:100];
|
||||
}
|
||||
+ (UIColor *)partner20TextColor
|
||||
{
|
||||
return [UIColor colorWithRed:scaled(255) green:scaled(255) blue:scaled(255) alpha:100];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
16
iphone/Maps/Categories/UIColorRoutines.h
Normal file
16
iphone/Maps/Categories/UIColorRoutines.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
static CGFloat const alpha04 = 0.04;
|
||||
static CGFloat const alpha12 = 0.12;
|
||||
static CGFloat const alpha20 = 0.20;
|
||||
static CGFloat const alpha26 = 0.26;
|
||||
static CGFloat const alpha30 = 0.3;
|
||||
static CGFloat const alpha32 = 0.32;
|
||||
static CGFloat const alpha36 = 0.36;
|
||||
static CGFloat const alpha40 = 0.4;
|
||||
static CGFloat const alpha54 = 0.54;
|
||||
static CGFloat const alpha70 = 0.7;
|
||||
static CGFloat const alpha80 = 0.8;
|
||||
static CGFloat const alpha87 = 0.87;
|
||||
static CGFloat const alpha90 = 0.9;
|
||||
static CGFloat const alpha100 = 1.;
|
||||
|
||||
static inline CGFloat scaled(CGFloat f) { return f / 255.; };
|
||||
29
iphone/Maps/Categories/UIFont+MapsMeFonts.h
Normal file
29
iphone/Maps/Categories/UIFont+MapsMeFonts.h
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
NS_ASSUME_NONNULL_BEGIN
|
||||
@interface UIFont (MapsMeFonts)
|
||||
|
||||
+ (UIFont *)regular10;
|
||||
+ (UIFont *)regular12;
|
||||
+ (UIFont *)regular13;
|
||||
+ (UIFont *)regular14;
|
||||
+ (UIFont *)regular16;
|
||||
+ (UIFont *)regular17;
|
||||
+ (UIFont *)regular18;
|
||||
+ (UIFont *)regular24;
|
||||
+ (UIFont *)regular32;
|
||||
+ (UIFont *)regular52;
|
||||
+ (UIFont *)medium10;
|
||||
+ (UIFont *)medium14;
|
||||
+ (UIFont *)medium16;
|
||||
+ (UIFont *)medium17;
|
||||
+ (UIFont *)light12;
|
||||
+ (UIFont *)bold12;
|
||||
+ (UIFont *)bold14;
|
||||
+ (UIFont *)bold16;
|
||||
+ (UIFont *)bold17;
|
||||
+ (UIFont *)bold24;
|
||||
+ (UIFont *)bold28;
|
||||
+ (UIFont *)bold36;
|
||||
+ (UIFont *)semibold16;
|
||||
|
||||
@end
|
||||
NS_ASSUME_NONNULL_END
|
||||
27
iphone/Maps/Categories/UIFont+MapsMeFonts.m
Normal file
27
iphone/Maps/Categories/UIFont+MapsMeFonts.m
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
@implementation UIFont (MapsMeFonts)
|
||||
|
||||
+ (UIFont *)regular10 { return [UIFont systemFontOfSize:10]; }
|
||||
+ (UIFont *)regular12 { return [UIFont systemFontOfSize:12]; }
|
||||
+ (UIFont *)regular13 { return [UIFont systemFontOfSize:13]; }
|
||||
+ (UIFont *)regular14 { return [UIFont systemFontOfSize:14]; }
|
||||
+ (UIFont *)regular16 { return [UIFont systemFontOfSize:16]; }
|
||||
+ (UIFont *)regular17 { return [UIFont systemFontOfSize:17]; }
|
||||
+ (UIFont *)regular18 { return [UIFont systemFontOfSize:18]; }
|
||||
+ (UIFont *)regular24 { return [UIFont systemFontOfSize:24]; }
|
||||
+ (UIFont *)regular32 { return [UIFont systemFontOfSize:32]; }
|
||||
+ (UIFont *)regular52 { return [UIFont systemFontOfSize:52]; }
|
||||
+ (UIFont *)medium10 { return [UIFont systemFontOfSize:10 weight:UIFontWeightMedium]; }
|
||||
+ (UIFont *)medium14 { return [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; }
|
||||
+ (UIFont *)medium16 { return [UIFont systemFontOfSize:16 weight:UIFontWeightMedium]; }
|
||||
+ (UIFont *)medium17 { return [UIFont systemFontOfSize:17 weight:UIFontWeightMedium]; }
|
||||
+ (UIFont *)light12 { return [UIFont systemFontOfSize:12 weight:UIFontWeightLight]; }
|
||||
+ (UIFont *)bold12 { return [UIFont boldSystemFontOfSize:12]; }
|
||||
+ (UIFont *)bold14 { return [UIFont boldSystemFontOfSize:14]; }
|
||||
+ (UIFont *)bold16 { return [UIFont boldSystemFontOfSize:16]; }
|
||||
+ (UIFont *)bold17 { return [UIFont boldSystemFontOfSize:17]; }
|
||||
+ (UIFont *)bold24 { return [UIFont boldSystemFontOfSize:24]; }
|
||||
+ (UIFont *)bold28 { return [UIFont boldSystemFontOfSize:28]; }
|
||||
+ (UIFont *)bold36 { return [UIFont boldSystemFontOfSize:26]; }
|
||||
+ (UIFont *)semibold16 { return [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; }
|
||||
|
||||
@end
|
||||
9
iphone/Maps/Categories/UIImage+FilledWithColor.swift
Normal file
9
iphone/Maps/Categories/UIImage+FilledWithColor.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
extension UIImage {
|
||||
static func filled(with color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
return renderer.image { context in
|
||||
color.setFill()
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
iphone/Maps/Categories/UIImage+RGBAData.h
Normal file
5
iphone/Maps/Categories/UIImage+RGBAData.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
@interface UIImage (RGBAData)
|
||||
|
||||
+ (UIImage *)imageWithRGBAData:(NSData *)data width:(size_t)width height:(size_t)height;
|
||||
|
||||
@end
|
||||
36
iphone/Maps/Categories/UIImage+RGBAData.m
Normal file
36
iphone/Maps/Categories/UIImage+RGBAData.m
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#import "UIImage+RGBAData.h"
|
||||
|
||||
static void releaseCallback(void *info, const void *data, size_t size) {
|
||||
CFRelease((CFDataRef)info);
|
||||
}
|
||||
|
||||
@implementation UIImage (RGBAData)
|
||||
|
||||
+ (UIImage *)imageWithRGBAData:(NSData *)data width:(size_t)width height:(size_t)height {
|
||||
size_t bytesPerPixel = 4;
|
||||
size_t bitsPerComponent = 8;
|
||||
size_t bitsPerPixel = bitsPerComponent * bytesPerPixel;
|
||||
size_t bytesPerRow = bytesPerPixel * width;
|
||||
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaLast;
|
||||
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
CFDataRef cfData = (__bridge_retained CFDataRef)data;
|
||||
CGDataProviderRef provider = CGDataProviderCreateWithData((void *)cfData,
|
||||
data.bytes,
|
||||
height * bytesPerRow,
|
||||
releaseCallback);
|
||||
|
||||
CGImageRef cgImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow,
|
||||
colorSpace, bitmapInfo, provider,
|
||||
NULL, YES, kCGRenderingIntentDefault);
|
||||
|
||||
UIImage *image = [UIImage imageWithCGImage:cgImage];
|
||||
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
CGDataProviderRelease(provider);
|
||||
CGImageRelease(cgImage);
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
@end
|
||||
17
iphone/Maps/Categories/UIImageView+Coloring.h
Normal file
17
iphone/Maps/Categories/UIImageView+Coloring.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
typedef NS_ENUM(NSUInteger, MWMImageColoring) {
|
||||
MWMImageColoringOther,
|
||||
MWMImageColoringBlue,
|
||||
MWMImageColoringBlack,
|
||||
MWMImageColoringWhite,
|
||||
MWMImageColoringGray,
|
||||
MWMImageColoringSeparator
|
||||
};
|
||||
|
||||
@interface UIImageView (Coloring)
|
||||
|
||||
@property(nonatomic) MWMImageColoring mwm_coloring;
|
||||
@property(copy, nonatomic) NSString * mwm_name;
|
||||
|
||||
- (void)changeColoringToOpposite;
|
||||
|
||||
@end
|
||||
101
iphone/Maps/Categories/UIImageView+Coloring.m
Normal file
101
iphone/Maps/Categories/UIImageView+Coloring.m
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
#import "UIImageView+Coloring.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation UIImageView (Coloring)
|
||||
|
||||
- (void)setMwm_name:(NSString *)mwm_name
|
||||
{
|
||||
objc_setAssociatedObject(self, @selector(mwm_name), mwm_name, OBJC_ASSOCIATION_COPY_NONATOMIC);
|
||||
self.image =
|
||||
[UIImage imageNamed:[NSString stringWithFormat:@"%@_%@", mwm_name,
|
||||
[UIColor isNightMode] ? @"dark" : @"light"]];
|
||||
}
|
||||
|
||||
- (NSString *)mwm_name { return objc_getAssociatedObject(self, @selector(mwm_name)); }
|
||||
- (void)setMwm_coloring:(MWMImageColoring)mwm_coloring
|
||||
{
|
||||
objc_setAssociatedObject(self, @selector(mwm_coloring), @(mwm_coloring),
|
||||
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
if (mwm_coloring == MWMImageColoringOther)
|
||||
return;
|
||||
[self applyColoring];
|
||||
}
|
||||
|
||||
- (MWMImageColoring)mwm_coloring
|
||||
{
|
||||
return [objc_getAssociatedObject(self, @selector(mwm_coloring)) integerValue];
|
||||
}
|
||||
|
||||
- (void)applyColoring
|
||||
{
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
self.tintColor = [[UIColor class] performSelector:self.coloringSelector];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
- (void)changeColoringToOpposite
|
||||
{
|
||||
if (self.mwm_coloring == MWMImageColoringOther)
|
||||
{
|
||||
if (self.mwm_name)
|
||||
self.image = [UIImage
|
||||
imageNamed:[NSString stringWithFormat:@"%@_%@", self.mwm_name,
|
||||
[UIColor isNightMode] ? @"dark" : @"light"]];
|
||||
return;
|
||||
}
|
||||
[self applyColoring];
|
||||
}
|
||||
|
||||
- (SEL)coloringSelector
|
||||
{
|
||||
switch (self.mwm_coloring)
|
||||
{
|
||||
case MWMImageColoringWhite: return @selector(white);
|
||||
case MWMImageColoringBlack: return @selector(blackSecondaryText);
|
||||
case MWMImageColoringBlue: return @selector(linkBlue);
|
||||
case MWMImageColoringGray: return @selector(blackHintText);
|
||||
case MWMImageColoringOther: return @selector(white);
|
||||
case MWMImageColoringSeparator: return @selector(blackDividers);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted
|
||||
{
|
||||
switch (self.mwm_coloring)
|
||||
{
|
||||
case MWMImageColoringWhite:
|
||||
self.tintColor = highlighted ? [UIColor whiteHintText] : [UIColor white];
|
||||
break;
|
||||
case MWMImageColoringBlack:
|
||||
self.tintColor = highlighted ? [UIColor blackHintText] : [UIColor blackSecondaryText];
|
||||
break;
|
||||
case MWMImageColoringGray:
|
||||
self.tintColor = highlighted ? [UIColor blackSecondaryText] : [UIColor blackHintText];
|
||||
break;
|
||||
case MWMImageColoringOther:
|
||||
case MWMImageColoringBlue:
|
||||
case MWMImageColoringSeparator: break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setColoring:(NSString *)coloring
|
||||
{
|
||||
if ([coloring isEqualToString:@"MWMBlue"])
|
||||
self.mwm_coloring = MWMImageColoringBlue;
|
||||
else if ([coloring isEqualToString:@"MWMBlack"])
|
||||
self.mwm_coloring = MWMImageColoringBlack;
|
||||
else if ([coloring isEqualToString:@"MWMOther"])
|
||||
self.mwm_coloring = MWMImageColoringOther;
|
||||
else if ([coloring isEqualToString:@"MWMGray"])
|
||||
self.mwm_coloring = MWMImageColoringGray;
|
||||
else if ([coloring isEqualToString:@"MWMSeparator"])
|
||||
self.mwm_coloring = MWMImageColoringSeparator;
|
||||
else if ([coloring isEqualToString:@"MWMWhite"])
|
||||
self.mwm_coloring = MWMImageColoringWhite;
|
||||
else
|
||||
NSAssert(false, @"Incorrect UIImageView's coloring");
|
||||
}
|
||||
|
||||
@end
|
||||
89
iphone/Maps/Categories/UIKitCategories.h
Normal file
89
iphone/Maps/Categories/UIKitCategories.h
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
#import <CoreApi/MWMTypes.h>
|
||||
|
||||
static inline CGPoint SubtractCGPoint(CGPoint p1, CGPoint p2)
|
||||
{
|
||||
return CGPointMake(p1.x - p2.x, p1.y - p2.y);
|
||||
}
|
||||
|
||||
static inline CGPoint AddCGPoint(CGPoint p1, CGPoint p2)
|
||||
{
|
||||
return CGPointMake(p1.x + p2.x, p1.y + p2.y);
|
||||
}
|
||||
|
||||
static inline CGPoint MultiplyCGPoint(CGPoint point, CGFloat multiplier)
|
||||
{
|
||||
return CGPointMake(point.x * multiplier, point.y * multiplier);
|
||||
}
|
||||
|
||||
static inline CGFloat LengthCGPoint(CGPoint point)
|
||||
{
|
||||
return (CGFloat)sqrt(point.x * point.x + point.y * point.y);
|
||||
}
|
||||
|
||||
@interface NSObject (Optimized)
|
||||
|
||||
+ (NSString *)className;
|
||||
- (void)performAfterDelay:(NSTimeInterval)delayInSec block:(MWMVoidBlock)block;
|
||||
|
||||
@end
|
||||
|
||||
@interface UIView (Coordinates)
|
||||
|
||||
@property (nonatomic) CGFloat minX;
|
||||
@property (nonatomic) CGFloat minY;
|
||||
@property (nonatomic) CGFloat midX;
|
||||
@property (nonatomic) CGFloat midY;
|
||||
@property (nonatomic) CGFloat maxX;
|
||||
@property (nonatomic) CGFloat maxY;
|
||||
@property (nonatomic) CGPoint origin;
|
||||
@property (nonatomic) CGFloat width;
|
||||
@property (nonatomic) CGFloat height;
|
||||
@property (nonatomic) CGSize size;
|
||||
|
||||
- (void)sizeToIntegralFit;
|
||||
|
||||
@end
|
||||
|
||||
@interface UIView (Refresh)
|
||||
|
||||
@end
|
||||
|
||||
@interface UIApplication (URLs)
|
||||
|
||||
- (void)rateApp;
|
||||
|
||||
@end
|
||||
|
||||
@interface SolidTouchView : UIView
|
||||
|
||||
@end
|
||||
|
||||
@interface SolidTouchImageView : UIImageView
|
||||
|
||||
@end
|
||||
|
||||
@interface UIViewController (Safari)
|
||||
|
||||
/// Open URL internally in SFSafariViewController. Returns NO (false) if the url id invalid.
|
||||
- (BOOL)openUrl:(NSString *)urlString;
|
||||
|
||||
/// Open URL externally in installed application (or in Safari if there are no appropriate application) if possible or internally in SFSafariViewController. Returns NO (false) if the url id invalid.
|
||||
///
|
||||
/// @param urlString: URL string to open.
|
||||
/// @param externally: If true, try to open URL in installed application or in Safari, otherwise open in internal browser without leaving the app.
|
||||
- (BOOL)openUrl:(NSString *)urlString externally:(BOOL)externally;
|
||||
|
||||
/// Open URL externally in installed application (or in Safari if there are no appropriate application) if possible or internally in SFSafariViewController. Returns NO (false) if the url id invalid.
|
||||
///
|
||||
/// @param urlString: URL string to open.
|
||||
/// @param externally: If true, try to open URL in installed application or in Safari, otherwise open in internal browser without leaving the app.
|
||||
/// @param skipEncoding: If true, extra URL encoding will be skipped
|
||||
- (BOOL)openUrl:(NSString *)urlString externally:(BOOL)externally skipEncoding:(BOOL)skipEncoding;
|
||||
|
||||
@end
|
||||
|
||||
@interface UIImage (ImageWithColor)
|
||||
|
||||
+ (UIImage *)imageWithColor:(UIColor *)color;
|
||||
|
||||
@end
|
||||
249
iphone/Maps/Categories/UIKitCategories.m
Normal file
249
iphone/Maps/Categories/UIKitCategories.m
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
#import "UIKitCategories.h"
|
||||
#import "UIButton+RuntimeAttributes.h"
|
||||
#import "UIImageView+Coloring.h"
|
||||
|
||||
#import <SafariServices/SafariServices.h>
|
||||
|
||||
@implementation NSObject (Optimized)
|
||||
|
||||
+ (NSString *)className { return NSStringFromClass(self); }
|
||||
- (void)performAfterDelay:(NSTimeInterval)delayInSec block:(MWMVoidBlock)block
|
||||
{
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSec * NSEC_PER_SEC)),
|
||||
dispatch_get_main_queue(), ^{
|
||||
block();
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIView (Coordinates)
|
||||
|
||||
- (void)setMidX:(CGFloat)midX { self.center = CGPointMake(midX, self.center.y); }
|
||||
- (CGFloat)midX { return self.center.x; }
|
||||
- (void)setMidY:(CGFloat)midY { self.center = CGPointMake(self.center.x, midY); }
|
||||
- (CGFloat)midY { return self.center.y; }
|
||||
- (void)setOrigin:(CGPoint)origin
|
||||
{
|
||||
self.frame = CGRectMake(origin.x, origin.y, self.frame.size.width, self.frame.size.height);
|
||||
}
|
||||
|
||||
- (CGPoint)origin { return self.frame.origin; }
|
||||
- (void)setMinX:(CGFloat)minX
|
||||
{
|
||||
self.frame = CGRectMake(minX, self.frame.origin.y, self.frame.size.width, self.frame.size.height);
|
||||
}
|
||||
|
||||
- (CGFloat)minX { return self.frame.origin.x; }
|
||||
- (void)setMinY:(CGFloat)minY
|
||||
{
|
||||
self.frame = CGRectMake(self.frame.origin.x, minY, self.frame.size.width, self.frame.size.height);
|
||||
}
|
||||
|
||||
- (CGFloat)minY { return self.frame.origin.y; }
|
||||
- (void)setMaxX:(CGFloat)maxX
|
||||
{
|
||||
self.frame = CGRectMake(maxX - self.frame.size.width, self.frame.origin.y, self.frame.size.width,
|
||||
self.frame.size.height);
|
||||
}
|
||||
|
||||
- (CGFloat)maxX { return self.frame.origin.x + self.frame.size.width; }
|
||||
- (void)setMaxY:(CGFloat)maxY
|
||||
{
|
||||
self.frame = CGRectMake(self.frame.origin.x, maxY - self.frame.size.height, self.frame.size.width,
|
||||
self.frame.size.height);
|
||||
}
|
||||
|
||||
- (CGFloat)maxY { return self.frame.origin.y + self.frame.size.height; }
|
||||
- (void)setWidth:(CGFloat)width
|
||||
{
|
||||
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, width, self.frame.size.height);
|
||||
}
|
||||
|
||||
- (CGFloat)width { return self.frame.size.width; }
|
||||
- (void)setHeight:(CGFloat)height
|
||||
{
|
||||
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, height);
|
||||
}
|
||||
|
||||
- (CGFloat)height { return self.frame.size.height; }
|
||||
- (CGSize)size { return self.frame.size; }
|
||||
- (void)setSize:(CGSize)size
|
||||
{
|
||||
self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, size.width, size.height);
|
||||
}
|
||||
|
||||
- (void)sizeToIntegralFit
|
||||
{
|
||||
[self sizeToFit];
|
||||
self.frame = CGRectIntegral(self.frame);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIApplication (URLs)
|
||||
|
||||
- (void)rateApp
|
||||
{
|
||||
NSString * urlString = @"https://apps.apple.com/app/comaps/id6747180809?action=write-review";
|
||||
NSURL * url = [NSURL URLWithString:urlString];
|
||||
[self openURL:url options:@{} completionHandler:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation SolidTouchView
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {}
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {}
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {}
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {}
|
||||
@end
|
||||
|
||||
@implementation UIView (Refresh)
|
||||
|
||||
@end
|
||||
|
||||
@implementation UITableViewCell (Refresh)
|
||||
|
||||
@end
|
||||
|
||||
@implementation UINavigationBar (Refresh)
|
||||
|
||||
@end
|
||||
|
||||
@implementation UILabel (Refresh)
|
||||
|
||||
@end
|
||||
|
||||
@implementation UISlider (Refresh)
|
||||
|
||||
@end
|
||||
|
||||
@implementation UISwitch (Refresh)
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIButton (Refresh)
|
||||
|
||||
@end
|
||||
|
||||
@implementation UITextView (Refresh)
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIImageView (Refresh)
|
||||
|
||||
@end
|
||||
|
||||
@implementation SolidTouchImageView
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {}
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {}
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {}
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {}
|
||||
@end
|
||||
|
||||
@implementation UINavigationController (Autorotate)
|
||||
|
||||
- (BOOL)shouldAutorotate { return [self.viewControllers.lastObject shouldAutorotate]; }
|
||||
@end
|
||||
|
||||
@implementation UIViewController (Autorotate)
|
||||
|
||||
- (NSUInteger)supportedInterfaceOrientations { return UIInterfaceOrientationMaskAll; }
|
||||
@end
|
||||
|
||||
@interface UIViewController (SafariDelegateImpl)<SFSafariViewControllerDelegate>
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIViewController (SafariDelegateImpl)
|
||||
|
||||
- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller
|
||||
{
|
||||
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIViewController (Safari)
|
||||
|
||||
- (BOOL)openUrl:(NSString * _Nonnull)urlString
|
||||
{
|
||||
return [self openUrl:urlString externally:NO];
|
||||
}
|
||||
|
||||
- (BOOL)openUrl:(NSString *)urlString externally:(BOOL)externally
|
||||
{
|
||||
return [self openUrl:urlString externally:externally skipEncoding:NO];
|
||||
}
|
||||
|
||||
- (BOOL)openUrl:(NSString *)urlString externally:(BOOL)externally skipEncoding:(BOOL)skipEncoding
|
||||
{
|
||||
NSString * encoded = urlString;
|
||||
if (!skipEncoding && ![urlString canBeConvertedToEncoding:NSASCIIStringEncoding]) {
|
||||
// TODO: This is a temporary workaround to open cyrillic/non-ASCII URLs.
|
||||
// URLs in OSM are stored in UTF-8. NSURL constructor documentation says:
|
||||
// > Must be a URL that conforms to RFC 2396. This method parses URLString according to RFCs 1738 and 1808.
|
||||
// The right way to encode the URL string should be:
|
||||
// 1. Split the (non-ASCII) string into components (host, path, query, fragment, etc.)
|
||||
// 2. Encode each component separately (they have different allowed characters).
|
||||
// 3. Merge them back into the string and create NSURL.
|
||||
NSMutableCharacterSet * charset = [[NSMutableCharacterSet alloc] init];
|
||||
[charset formUnionWithCharacterSet:NSCharacterSet.URLHostAllowedCharacterSet];
|
||||
[charset formUnionWithCharacterSet:NSCharacterSet.URLPathAllowedCharacterSet];
|
||||
[charset formUnionWithCharacterSet:NSCharacterSet.URLQueryAllowedCharacterSet];
|
||||
[charset formUnionWithCharacterSet:NSCharacterSet.URLFragmentAllowedCharacterSet];
|
||||
[charset addCharactersInString:@"#;/?:@&=+$,"];
|
||||
encoded = [urlString stringByAddingPercentEncodingWithAllowedCharacters:charset];
|
||||
}
|
||||
|
||||
// Matrix has an url with two hashes which doesn't work for NSURL and NSURLComponent.
|
||||
NSRange const matrixUrl = [encoded rangeOfString:@"#/#"];
|
||||
if (matrixUrl.location != NSNotFound)
|
||||
encoded = [encoded stringByReplacingOccurrencesOfString:@"#/#" withString:@"#/%23"];
|
||||
NSURLComponents * urlc = [NSURLComponents componentsWithString:encoded];
|
||||
if (!urlc)
|
||||
{
|
||||
NSAssert(false, @"Invalid URL %@", urlString);
|
||||
return NO;
|
||||
}
|
||||
// Some links in OSM do not have a scheme: www.some.link
|
||||
if (!urlc.scheme)
|
||||
urlc.scheme = @"http";
|
||||
|
||||
NSURL * url = urlc.URL;
|
||||
if (externally && [UIApplication.sharedApplication canOpenURL:url])
|
||||
{
|
||||
[UIApplication.sharedApplication openURL:url options:@{} completionHandler:nil];
|
||||
}
|
||||
else
|
||||
{
|
||||
SFSafariViewController * svc = [[SFSafariViewController alloc] initWithURL:url];
|
||||
svc.delegate = self;
|
||||
[self.navigationController presentViewController:svc animated:YES completion:nil];
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation UIImage (ImageWithColor)
|
||||
|
||||
+ (UIImage *)imageWithColor:(UIColor *)color
|
||||
{
|
||||
CGRect rect = CGRectMake(0.0, 0.0, 1.0, 1.0);
|
||||
UIGraphicsBeginImageContext(rect.size);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
|
||||
CGContextSetFillColorWithColor(context, color.CGColor);
|
||||
CGContextFillRect(context, rect);
|
||||
|
||||
UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
extension UILabel {
|
||||
var numberOfVisibleLines: Int {
|
||||
let textSize = CGSize(width: frame.size.width, height: CGFloat(MAXFLOAT))
|
||||
let rowHeight = sizeThatFits(textSize).height.rounded()
|
||||
let charHeight = font.pointSize.rounded()
|
||||
return Int((rowHeight / charHeight).rounded())
|
||||
}
|
||||
}
|
||||
3
iphone/Maps/Categories/UILabel+RuntimeAttributes.h
Normal file
3
iphone/Maps/Categories/UILabel+RuntimeAttributes.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@interface UILabel (RuntimeAttributes)
|
||||
@property (copy, nonatomic) NSString * localizedText;
|
||||
@end
|
||||
17
iphone/Maps/Categories/UILabel+RuntimeAttributes.m
Normal file
17
iphone/Maps/Categories/UILabel+RuntimeAttributes.m
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#import "UILabel+RuntimeAttributes.h"
|
||||
|
||||
// Runtime attributes for setting localized text in Xib.
|
||||
|
||||
@implementation UILabel (RuntimeAttributes)
|
||||
|
||||
- (void)setLocalizedText:(NSString *)localizedText
|
||||
{
|
||||
self.text = L(localizedText);
|
||||
}
|
||||
- (NSString *)localizedText
|
||||
{
|
||||
NSString * text = self.text;
|
||||
return L(text);
|
||||
}
|
||||
|
||||
@end
|
||||
5
iphone/Maps/Categories/UINib+Init.swift
Normal file
5
iphone/Maps/Categories/UINib+Init.swift
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
extension UINib {
|
||||
@objc convenience init(_ viewClass: AnyClass, bundle: Bundle? = nil) {
|
||||
self.init(nibName: toString(viewClass), bundle: bundle)
|
||||
}
|
||||
}
|
||||
45
iphone/Maps/Categories/UITableView+Cells.swift
Normal file
45
iphone/Maps/Categories/UITableView+Cells.swift
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
extension UITableView {
|
||||
@objc func registerNib(cellClass: AnyClass) {
|
||||
register(UINib(cellClass), forCellReuseIdentifier: toString(cellClass))
|
||||
}
|
||||
|
||||
@objc func registerClass(cellClass: AnyClass) {
|
||||
register(cellClass, forCellReuseIdentifier: toString(cellClass))
|
||||
}
|
||||
|
||||
@objc func dequeueReusableCell(withCellClass cellClass: AnyClass) -> UITableViewCell? {
|
||||
return dequeueReusableCell(withIdentifier: toString(cellClass))
|
||||
}
|
||||
|
||||
@objc func dequeueReusableCell(withCellClass cellClass: AnyClass, indexPath: IndexPath) -> UITableViewCell {
|
||||
return dequeueReusableCell(withIdentifier: toString(cellClass), for: indexPath)
|
||||
}
|
||||
|
||||
func registerNib<Cell>(cell: Cell.Type) where Cell: UITableViewCell {
|
||||
register(UINib(cell), forCellReuseIdentifier: toString(cell))
|
||||
}
|
||||
|
||||
func registerNibs<Cell>(_ cells: [Cell.Type]) where Cell: UITableViewCell {
|
||||
cells.forEach { registerNib(cell: $0) }
|
||||
}
|
||||
|
||||
func register<Cell>(cell: Cell.Type) where Cell: UITableViewCell {
|
||||
register(cell, forCellReuseIdentifier: toString(cell))
|
||||
}
|
||||
|
||||
func dequeueReusableCell<Cell>(cell: Cell.Type) -> Cell? where Cell: UITableViewCell {
|
||||
return dequeueReusableCell(withIdentifier: toString(cell)) as? Cell
|
||||
}
|
||||
|
||||
func dequeueReusableCell<Cell>(cell: Cell.Type, indexPath: IndexPath) -> Cell where Cell: UITableViewCell {
|
||||
return dequeueReusableCell(withIdentifier: toString(cell), for: indexPath) as! Cell
|
||||
}
|
||||
|
||||
func registerNibForHeaderFooterView<View>(_ view: View.Type) where View: UIView {
|
||||
register(UINib(view), forHeaderFooterViewReuseIdentifier: toString(view))
|
||||
}
|
||||
|
||||
func dequeueReusableHeaderFooterView<View>(_ view: View.Type) -> View where View: UIView {
|
||||
return dequeueReusableHeaderFooterView(withIdentifier: toString(view)) as! View
|
||||
}
|
||||
}
|
||||
18
iphone/Maps/Categories/UITableView+Updates.swift
Normal file
18
iphone/Maps/Categories/UITableView+Updates.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
extension UITableView {
|
||||
typealias Updates = () -> Void
|
||||
typealias Completion = () -> Void
|
||||
|
||||
@objc func update(_ updates: Updates) {
|
||||
performBatchUpdates(updates, completion: nil)
|
||||
}
|
||||
|
||||
@objc func update(_ updates: Updates, completion: @escaping Completion) {
|
||||
performBatchUpdates(updates, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
@objc func refresh() {
|
||||
update {}
|
||||
}
|
||||
}
|
||||
5
iphone/Maps/Categories/UITextField+RuntimeAttributes.h
Normal file
5
iphone/Maps/Categories/UITextField+RuntimeAttributes.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
@interface UITextField (RuntimeAttributes)
|
||||
|
||||
@property (copy, nonatomic) NSString * localizedPlaceholder;
|
||||
|
||||
@end
|
||||
16
iphone/Maps/Categories/UITextField+RuntimeAttributes.m
Normal file
16
iphone/Maps/Categories/UITextField+RuntimeAttributes.m
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#import "UITextField+RuntimeAttributes.h"
|
||||
|
||||
@implementation UITextField (RuntimeAttributes)
|
||||
|
||||
- (void)setLocalizedPlaceholder:(NSString *)placeholder
|
||||
{
|
||||
self.placeholder = L(placeholder);
|
||||
}
|
||||
|
||||
- (NSString *)localizedPlaceholder
|
||||
{
|
||||
NSString * placeholder = self.placeholder;
|
||||
return L(placeholder);
|
||||
}
|
||||
|
||||
@end
|
||||
11
iphone/Maps/Categories/UITextView+RuntimeAttributes.h
Normal file
11
iphone/Maps/Categories/UITextView+RuntimeAttributes.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#import "MWMTextView.h"
|
||||
|
||||
@interface UITextView (UITextView_RuntimeAttributes)
|
||||
|
||||
@end
|
||||
|
||||
@interface MWMTextView (RuntimeAttributes)
|
||||
|
||||
@property (copy, nonatomic) NSString * localizedPlaceholder;
|
||||
|
||||
@end
|
||||
27
iphone/Maps/Categories/UITextView+RuntimeAttributes.m
Normal file
27
iphone/Maps/Categories/UITextView+RuntimeAttributes.m
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#import "UITextView+RuntimeAttributes.h"
|
||||
|
||||
@implementation UITextView (RuntimeAttributes)
|
||||
|
||||
- (void)setLocalizedText:(NSString *)localizedText
|
||||
{
|
||||
self.text = L(localizedText);
|
||||
}
|
||||
|
||||
- (NSString *)localizedText
|
||||
{
|
||||
return L(self.text);
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation MWMTextView (RuntimeAttributes)
|
||||
|
||||
- (void)setLocalizedPlaceholder:(NSString *)localizedPlaceholder
|
||||
{
|
||||
self.placeholder = L(localizedPlaceholder);
|
||||
}
|
||||
|
||||
- (NSString *)localizedPlaceholder
|
||||
{
|
||||
return L(self.placeholder);
|
||||
}
|
||||
@end
|
||||
30
iphone/Maps/Categories/UIView+AddSeparator.swift
Normal file
30
iphone/Maps/Categories/UIView+AddSeparator.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
extension UIView {
|
||||
enum SeparatorPosition {
|
||||
case top
|
||||
case bottom
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addSeparator(_ position: SeparatorPosition = .top,
|
||||
thickness: CGFloat = 1.0,
|
||||
insets: UIEdgeInsets = .zero) -> UIView {
|
||||
let lineView = UIView()
|
||||
lineView.setStyleAndApply(.divider)
|
||||
lineView.isUserInteractionEnabled = false
|
||||
lineView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(lineView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
lineView.heightAnchor.constraint(equalToConstant: thickness),
|
||||
lineView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: insets.left),
|
||||
lineView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -insets.right),
|
||||
])
|
||||
switch position {
|
||||
case .top:
|
||||
lineView.topAnchor.constraint(equalTo: topAnchor, constant: insets.top).isActive = true
|
||||
case .bottom:
|
||||
lineView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom).isActive = true
|
||||
}
|
||||
return lineView
|
||||
}
|
||||
}
|
||||
40
iphone/Maps/Categories/UIView+Animation.swift
Normal file
40
iphone/Maps/Categories/UIView+Animation.swift
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
extension UIView {
|
||||
@objc func animateConstraints(duration: TimeInterval,
|
||||
animations: @escaping () -> Void,
|
||||
completion: @escaping () -> Void) {
|
||||
setNeedsLayout()
|
||||
UIView.animate(withDuration: duration,
|
||||
animations: { [weak self] in
|
||||
animations()
|
||||
self?.layoutIfNeeded()
|
||||
},
|
||||
completion: { _ in completion() })
|
||||
}
|
||||
|
||||
@objc func animateConstraints(animations: @escaping () -> Void, completion: @escaping () -> Void) {
|
||||
animateConstraints(duration: kDefaultAnimationDuration, animations: animations, completion: completion)
|
||||
}
|
||||
|
||||
@objc func animateConstraints(animations: @escaping () -> Void) {
|
||||
animateConstraints(duration: kDefaultAnimationDuration, animations: animations, completion: {})
|
||||
}
|
||||
|
||||
@objc func startRotation(_ duration: TimeInterval = 1.0) {
|
||||
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
rotationAnimation.toValue = Double.pi * 2
|
||||
rotationAnimation.duration = duration;
|
||||
rotationAnimation.isCumulative = true;
|
||||
rotationAnimation.repeatCount = Float.greatestFiniteMagnitude;
|
||||
rotationAnimation.isRemovedOnCompletion = false
|
||||
layer.add(rotationAnimation, forKey: "rotationAnimation")
|
||||
}
|
||||
|
||||
@objc func stopRotation() {
|
||||
layer.removeAnimation(forKey: "rotationAnimation")
|
||||
}
|
||||
|
||||
@objc var isRotating: Bool {
|
||||
return layer.animationKeys()?.contains("rotationAnimation") ?? false
|
||||
}
|
||||
}
|
||||
13
iphone/Maps/Categories/UIView+Coordinates.swift
Normal file
13
iphone/Maps/Categories/UIView+Coordinates.swift
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
extension UIView {
|
||||
func center(inContainerView containerView: UIView) -> CGPoint {
|
||||
guard let sv = superview else { return .zero }
|
||||
var centerPoint = center
|
||||
|
||||
if let scrollView = sv as? UIScrollView, scrollView.zoomScale != 1.0 {
|
||||
centerPoint.x += (scrollView.bounds.width - scrollView.contentSize.width) / 2.0 + scrollView.contentOffset.x
|
||||
centerPoint.y += (scrollView.bounds.height - scrollView.contentSize.height) / 2.0 + scrollView.contentOffset.y
|
||||
}
|
||||
return sv.convert(centerPoint, to: containerView)
|
||||
}
|
||||
}
|
||||
20
iphone/Maps/Categories/UIView+Hierarchy.swift
Normal file
20
iphone/Maps/Categories/UIView+Hierarchy.swift
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
extension UIView {
|
||||
@objc func hasSubview(viewClass: AnyClass) -> Bool {
|
||||
return !subviews.filter { type(of: $0) == viewClass }.isEmpty
|
||||
}
|
||||
|
||||
func clearTreeBackground() {
|
||||
backgroundColor = UIColor.clear
|
||||
subviews.forEach { $0.clearTreeBackground() }
|
||||
}
|
||||
|
||||
func alignToSuperview(_ insets: UIEdgeInsets = .zero) {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
topAnchor.constraint(equalTo: superview!.topAnchor, constant: insets.top),
|
||||
leftAnchor.constraint(equalTo: superview!.leftAnchor, constant: insets.left),
|
||||
bottomAnchor.constraint(equalTo: superview!.bottomAnchor, constant: insets.bottom),
|
||||
rightAnchor.constraint(equalTo: superview!.rightAnchor, constant: insets.right)
|
||||
])
|
||||
}
|
||||
}
|
||||
25
iphone/Maps/Categories/UIView+Highlight.swift
Normal file
25
iphone/Maps/Categories/UIView+Highlight.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
extension UIView {
|
||||
@objc
|
||||
func highlight() {
|
||||
let color = UIColor.linkBlueHighlighted().withAlphaComponent(0.2)
|
||||
let duration: TimeInterval = kDefaultAnimationDuration
|
||||
let overlayView = UIView(frame: bounds)
|
||||
overlayView.backgroundColor = color
|
||||
overlayView.alpha = 0
|
||||
overlayView.clipsToBounds = true
|
||||
overlayView.isUserInteractionEnabled = false
|
||||
overlayView.layer.cornerRadius = layer.cornerRadius
|
||||
overlayView.layer.maskedCorners = layer.maskedCorners
|
||||
addSubview(overlayView)
|
||||
|
||||
UIView.animate(withDuration: duration, delay: duration, options: .curveEaseInOut, animations: {
|
||||
overlayView.alpha = 1
|
||||
}) { _ in
|
||||
UIView.animate(withDuration: duration, delay: duration * 3, options: .curveEaseInOut, animations: {
|
||||
overlayView.alpha = 0
|
||||
}) { _ in
|
||||
overlayView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
iphone/Maps/Categories/UIView+Snapshot.swift
Normal file
22
iphone/Maps/Categories/UIView+Snapshot.swift
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
extension UIView {
|
||||
@objc var snapshot: UIView {
|
||||
guard let contents = layer.contents else {
|
||||
return snapshotView(afterScreenUpdates: true)!
|
||||
}
|
||||
let snapshot: UIView
|
||||
if let view = self as? UIImageView {
|
||||
snapshot = UIImageView(image: view.image)
|
||||
snapshot.bounds = view.bounds
|
||||
} else {
|
||||
snapshot = UIView(frame: frame)
|
||||
snapshot.layer.contents = contents
|
||||
snapshot.layer.bounds = layer.bounds
|
||||
}
|
||||
snapshot.layer.setCornerRadius(.custom(layer.cornerRadius))
|
||||
snapshot.layer.masksToBounds = layer.masksToBounds
|
||||
snapshot.contentMode = contentMode
|
||||
snapshot.transform = transform
|
||||
return snapshot
|
||||
}
|
||||
}
|
||||
12
iphone/Maps/Categories/UIViewController+Hierarchy.swift
Normal file
12
iphone/Maps/Categories/UIViewController+Hierarchy.swift
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
extension UIViewController {
|
||||
@objc static func topViewController() -> UIViewController {
|
||||
let window = UIApplication.shared.delegate!.window!!
|
||||
if var topController = window.rootViewController {
|
||||
while let presentedViewController = topController.presentedViewController {
|
||||
topController = presentedViewController
|
||||
}
|
||||
return topController
|
||||
}
|
||||
return (window.rootViewController as! UINavigationController).topViewController!
|
||||
}
|
||||
}
|
||||
8
iphone/Maps/Categories/UIViewController+Navigation.h
Normal file
8
iphone/Maps/Categories/UIViewController+Navigation.h
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
@interface UIViewController (Navigation)
|
||||
|
||||
- (void)goBack;
|
||||
|
||||
- (UIBarButtonItem *)buttonWithImage:(UIImage *)image action:(SEL)action;
|
||||
- (NSArray<UIBarButtonItem *> *)alignedNavBarButtonItems:(NSArray<UIBarButtonItem *> *)items;
|
||||
|
||||
@end
|
||||
30
iphone/Maps/Categories/UIViewController+Navigation.m
Normal file
30
iphone/Maps/Categories/UIViewController+Navigation.m
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#import "UIButton+Orientation.h"
|
||||
#import "UIViewController+Navigation.h"
|
||||
|
||||
static CGFloat const kButtonExtraWidth = 16.0;
|
||||
|
||||
@implementation UIViewController (Navigation)
|
||||
|
||||
- (UIBarButtonItem *)negativeSpacer
|
||||
{
|
||||
UIBarButtonItem * spacer =
|
||||
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace
|
||||
target:nil
|
||||
action:nil];
|
||||
spacer.width = -kButtonExtraWidth;
|
||||
return spacer;
|
||||
}
|
||||
|
||||
- (UIBarButtonItem *)buttonWithImage:(UIImage *)image action:(SEL)action
|
||||
{
|
||||
return [[UIBarButtonItem alloc] initWithImage:image style:UIBarButtonItemStylePlain target:self action:action];
|
||||
}
|
||||
|
||||
- (NSArray<UIBarButtonItem *> *)alignedNavBarButtonItems:(NSArray<UIBarButtonItem *> *)items
|
||||
{
|
||||
return [@[ [self negativeSpacer] ] arrayByAddingObjectsFromArray:items];
|
||||
}
|
||||
|
||||
- (void)goBack { [self.navigationController popViewControllerAnimated:YES]; }
|
||||
|
||||
@end
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue