Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:58:55 +01:00
parent 4af19165ec
commit 68073add76
12458 changed files with 12350765 additions and 2 deletions

View file

@ -0,0 +1,37 @@
@objc(MWMBorderedButton)
final class BorderedButton: UIButton {
private var borderColor: UIColor?
private var borderHighlightedColor: UIColor?
private var borderDisabledColor: UIColor?
@objc func setBorderColor(_ color: UIColor) {
borderColor = color
}
@objc func setBorderHighlightedColor(_ color: UIColor) {
borderHighlightedColor = color
}
@objc func setBorderDisabledColor(_ color: UIColor) {
borderDisabledColor = color
}
private func updateBorder() {
if !isEnabled {
layer.borderColor = borderDisabledColor?.cgColor ?? titleColor(for: .disabled)?.cgColor
} else if isHighlighted {
layer.borderColor = borderHighlightedColor?.cgColor ?? titleColor(for: .highlighted)?.cgColor
} else {
layer.borderColor = borderColor?.cgColor ?? titleColor(for: .normal)?.cgColor
}
}
override var isEnabled: Bool {
didSet { updateBorder() }
}
override var isHighlighted: Bool {
didSet { updateBorder() }
}
}

View file

@ -0,0 +1,112 @@
class Checkmark: UIControl {
private let imageView = UIImageView(frame: .zero)
@IBInspectable
var offImage: UIImage? {
didSet {
updateImage(animated: false)
}
}
@IBInspectable
var onImage: UIImage? {
didSet {
updateImage(animated: false)
}
}
@IBInspectable
var offTintColor: UIColor? {
didSet {
updateTintColor()
}
}
@IBInspectable
var onTintColor: UIColor? {
didSet {
updateTintColor()
}
}
@IBInspectable
var isChecked: Bool = false {
didSet {
updateImage(animated: true)
updateTintColor()
}
}
override var isHighlighted: Bool {
didSet {
imageView.tintColor = isHighlighted ? tintColor.blending(with: UIColor(white: 0, alpha: 0.5)) : nil
}
}
override init(frame: CGRect) {
super.init(frame: frame)
initViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initViews()
}
private func initViews() {
addSubview(imageView)
addTarget(self, action: #selector(onTouch), for: .touchUpInside)
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.sizeToFit()
var left: CGFloat = 0;
var top: CGFloat = 0;
var width: CGFloat = imageView.width;
var height: CGFloat = imageView.height;
switch contentHorizontalAlignment {
case .right: fallthrough
case .trailing:
left = floor(bounds.width - imageView.width)
case .center:
left = floor((bounds.width - width) / 2)
case .fill:
width = bounds.width
default:
left = 0
}
switch contentVerticalAlignment {
case .top:
top = 0
case .bottom:
top = floor(bounds.height - height)
case .center:
top = floor((bounds.height - height) / 2)
case .fill:
height = bounds.height
@unknown default:
fatalError("Unexpected case in contentVerticalAlignment switch")
}
imageView.frame = CGRect(x: left, y: top, width: width, height: height)
}
@objc func onTouch() {
isChecked = !isChecked
sendActions(for: .valueChanged)
}
private func updateImage(animated: Bool) {
self.imageView.image = self.isChecked ? self.onImage : self.offImage
}
private func updateTintColor() {
tintColor = isChecked ? onTintColor : offTintColor
}
}

View file

@ -0,0 +1,48 @@
// Label shows copy popup menu on tap or long tap.
class CopyableLabel: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
self.sharedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.sharedInit()
}
private func sharedInit() {
self.isUserInteractionEnabled = true
self.gestureRecognizers = [
UILongPressGestureRecognizer(target: self, action: #selector(self.showMenu)),
UITapGestureRecognizer(target: self, action: #selector(self.showMenu))
]
}
@objc func showMenu(_ recognizer: UILongPressGestureRecognizer) {
self.becomeFirstResponder()
let menu = UIMenuController.shared
let locationOfTouchInLabel = recognizer.location(in: self)
if !menu.isMenuVisible {
var rect = bounds
rect.origin = locationOfTouchInLabel
rect.size = CGSize(width: 1, height: 1)
menu.showMenu(from: self, rect: rect)
}
}
override func copy(_ sender: Any?) {
UIPasteboard.general.string = text
UIMenuController.shared.hideMenu()
}
override var canBecomeFirstResponder: Bool {
return true
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == #selector(UIResponderStandardEditActions.copy)
}
}

View file

@ -0,0 +1,42 @@
@objc(MWMDimBackground)
final class DimBackground: SolidTouchView {
private let mainView: UIView
private var tapAction: () -> Void
@objc init(mainView: UIView, tapAction: @escaping () -> Void) {
self.mainView = mainView
self.tapAction = tapAction
super.init(frame: mainView.superview!.bounds)
setStyle(.fadeBackground)
autoresizingMask = [.flexibleWidth, .flexibleHeight]
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func setVisible(_ visible: Bool, completion: (() -> Void)?) {
if visible {
let sv = mainView.superview!
frame = sv.bounds
sv.insertSubview(self, belowSubview: mainView)
alpha = 0
} else {
alpha = 0.8
}
UIView.animate(withDuration: kDefaultAnimationDuration,
animations: { self.alpha = visible ? 0.8 : 0 },
completion: { _ in
if !visible {
self.removeFromSuperview()
}
completion?()
})
}
@objc
private func onTap() {
tapAction()
}
}

View file

@ -0,0 +1,13 @@
@IBDesignable
class LeftAlignedIconButton: UIButton {
override func layoutSubviews() {
super.layoutSubviews()
contentHorizontalAlignment = .left
let availableSpace = bounds.inset(by: contentEdgeInsets)
let imageWidth = imageView?.frame.width ?? 0
let titleWidth = titleLabel?.frame.width ?? 0
let availableWidth = availableSpace.width - imageEdgeInsets.right - imageWidth * 2 - titleWidth
titleEdgeInsets = UIEdgeInsets(top: 0, left: floor(availableWidth) / 2, bottom: 0, right: 0)
}
}

View file

@ -0,0 +1,5 @@
class LinkTextView : UITextView {
override var canBecomeFirstResponder: Bool {
return false;
}
}

View file

@ -0,0 +1,11 @@
#include "geometry/point2d.hpp"
@interface MWMAddPlaceNavigationBar : SolidTouchView
+ (void)showInSuperview:(UIView *)superview
isBusiness:(BOOL)isBusiness
position:(m2::PointD const *)optionalPosition
doneBlock:(MWMVoidBlock)done
cancelBlock:(MWMVoidBlock)cancel;
@end

View file

@ -0,0 +1,76 @@
#import "MWMAddPlaceNavigationBar.h"
#include <CoreApi/Framework.h>
@interface MWMAddPlaceNavigationBar ()
@property(copy, nonatomic) MWMVoidBlock doneBlock;
@property(copy, nonatomic) MWMVoidBlock cancelBlock;
@property(assign, nonatomic) NSLayoutConstraint* topConstraint;
@end
@implementation MWMAddPlaceNavigationBar
+ (void)showInSuperview:(UIView *)superview
isBusiness:(BOOL)isBusiness
position:(m2::PointD const *)optionalPosition
doneBlock:(MWMVoidBlock)done
cancelBlock:(MWMVoidBlock)cancel
{
MWMAddPlaceNavigationBar * navBar =
[NSBundle.mainBundle loadNibNamed:self.className owner:nil options:nil].firstObject;
navBar.width = superview.width;
navBar.doneBlock = done;
navBar.cancelBlock = cancel;
navBar.translatesAutoresizingMaskIntoConstraints = false;
[superview addSubview:navBar];
navBar.topConstraint = [navBar.topAnchor constraintEqualToAnchor:superview.topAnchor];
navBar.topConstraint.active = true;
navBar.topConstraint.constant = -navBar.height;
[navBar.trailingAnchor constraintEqualToAnchor:superview.trailingAnchor].active = true;
[navBar.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor].active = true;
[navBar show:isBusiness position:optionalPosition];
}
- (void)show:(BOOL)enableBounds position:(m2::PointD const *)optionalPosition
{
auto & f = GetFramework();
f.EnableChoosePositionMode(true /* enable */, enableBounds, optionalPosition);
f.BlockTapEvents(true);
[UIView animateWithDuration:kDefaultAnimationDuration animations:^
{
self.topConstraint.constant = 0;
}];
}
- (void)dismissWithBlock:(MWMVoidBlock)block
{
auto & f = GetFramework();
f.EnableChoosePositionMode(false /* enable */, false /* enableBounds */, nullptr /* optionalPosition */);
f.BlockTapEvents(false);
[UIView animateWithDuration:kDefaultAnimationDuration animations:^
{
self.topConstraint.constant = -self.height;
}
completion:^(BOOL finished)
{
[self removeFromSuperview];
block();
}];
}
- (IBAction)doneTap
{
[self dismissWithBlock:self.doneBlock];
}
- (IBAction)cancelTap
{
[self dismissWithBlock:self.cancelBlock];
}
@end

View file

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="24128" 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="24063"/>
<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="BJ6-My-yAy" customClass="MWMAddPlaceNavigationBar" propertyAccessControl="none">
<rect key="frame" x="0.0" y="0.0" width="320" height="100"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IhK-ce-TXk">
<rect key="frame" x="0.0" y="0.0" width="320" height="64"/>
<color key="backgroundColor" red="0.1176470588" green="0.58823529409999997" blue="0.31372549020000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="PrimaryBackground"/>
</userDefinedRuntimeAttributes>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ZKD-Be-eMp">
<rect key="frame" x="0.0" y="20" width="320" height="44"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="1000" contentHorizontalAlignment="right" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="zs9-MX-bIe">
<rect key="frame" x="258" y="12" width="54" height="19"/>
<constraints>
<constraint firstAttribute="height" constant="19" id="psK-YU-lUF"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<state key="normal" title="Готово"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="NavigationBarItem"/>
<userDefinedRuntimeAttribute type="string" keyPath="localizedText" value="continue_button"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="doneTap" destination="BJ6-My-yAy" eventType="touchUpInside" id="hS1-Tz-6Zo"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="Местоположение" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8So-22-JS1">
<rect key="frame" x="90" y="12" width="140" height="20"/>
<constraints>
<constraint firstAttribute="height" constant="20" id="JFT-iW-rJY"/>
<constraint firstAttribute="height" constant="20" id="mun-d1-wdT"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="NavigationBarItem"/>
<userDefinedRuntimeAttribute type="string" keyPath="localizedText" value="editor_add_select_location"/>
</userDefinedRuntimeAttributes>
</label>
<button opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="1000" contentHorizontalAlignment="left" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="boX-sU-7DW">
<rect key="frame" x="8" y="12" width="62" height="19"/>
<constraints>
<constraint firstAttribute="height" constant="19" id="KDz-RG-UH2"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<state key="normal" title="Отмена"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="NavigationBarItem"/>
<userDefinedRuntimeAttribute type="string" keyPath="localizedText" value="cancel"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="cancelTap" destination="BJ6-My-yAy" eventType="touchUpInside" id="f2O-El-juu"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="0.1176470588" green="0.58823529409999997" blue="0.31372549020000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="5VI-KR-aXh"/>
<constraint firstItem="zs9-MX-bIe" firstAttribute="top" secondItem="ZKD-Be-eMp" secondAttribute="top" constant="12" id="BNr-Gr-o3j"/>
<constraint firstItem="8So-22-JS1" firstAttribute="top" secondItem="ZKD-Be-eMp" secondAttribute="top" constant="12" id="CCm-fM-FcD"/>
<constraint firstItem="boX-sU-7DW" firstAttribute="top" secondItem="ZKD-Be-eMp" secondAttribute="top" constant="12" id="Ofu-8n-bBo"/>
<constraint firstItem="boX-sU-7DW" firstAttribute="leading" secondItem="ZKD-Be-eMp" secondAttribute="leading" constant="8" id="Sbg-dn-iro"/>
<constraint firstAttribute="trailing" secondItem="zs9-MX-bIe" secondAttribute="trailing" constant="8" id="jiJ-gk-0bx"/>
<constraint firstItem="8So-22-JS1" firstAttribute="centerX" secondItem="ZKD-Be-eMp" secondAttribute="centerX" id="m6T-st-PPz"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="PrimaryBackground"/>
</userDefinedRuntimeAttributes>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="UsM-Al-MCC">
<rect key="frame" x="0.0" y="64" width="320" height="56"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Потяните карту, чтобы выбрать правильное местоположение места. " lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="R6E-kO-6Lb">
<rect key="frame" x="14" y="16" width="292" height="30"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" systemColor="darkTextColor"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="regular12:blackSecondaryText"/>
<userDefinedRuntimeAttribute type="string" keyPath="localizedText" value="editor_focus_map_on_location"/>
</userDefinedRuntimeAttributes>
</label>
</subviews>
<color key="backgroundColor" red="0.96078431372549022" green="0.96078431372549022" blue="0.96078431372549022" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="R6E-kO-6Lb" firstAttribute="leading" secondItem="UsM-Al-MCC" secondAttribute="leadingMargin" constant="6" id="F0k-Ua-uzh"/>
<constraint firstItem="R6E-kO-6Lb" firstAttribute="top" secondItem="UsM-Al-MCC" secondAttribute="topMargin" constant="8" id="G1O-ji-PsY"/>
<constraint firstAttribute="height" constant="56" id="RVj-Yg-4Hc"/>
<constraint firstAttribute="trailingMargin" secondItem="R6E-kO-6Lb" secondAttribute="trailing" constant="6" id="cRp-da-FUC"/>
<constraint firstAttribute="bottomMargin" secondItem="R6E-kO-6Lb" secondAttribute="bottom" constant="2" id="e1g-Jr-sY9"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MenuBackground"/>
</userDefinedRuntimeAttributes>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="tvs-e8-vSU"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="top" secondItem="IhK-ce-TXk" secondAttribute="top" id="B9q-oF-68E"/>
<constraint firstItem="UsM-Al-MCC" firstAttribute="leading" secondItem="BJ6-My-yAy" secondAttribute="leading" id="C3j-SM-MSO"/>
<constraint firstItem="ZKD-Be-eMp" firstAttribute="leading" secondItem="BJ6-My-yAy" secondAttribute="leading" id="Ckp-sr-B7c"/>
<constraint firstItem="ZKD-Be-eMp" firstAttribute="top" secondItem="tvs-e8-vSU" secondAttribute="top" id="Drs-L4-75n"/>
<constraint firstItem="IhK-ce-TXk" firstAttribute="leading" secondItem="tvs-e8-vSU" secondAttribute="leading" id="E1h-R5-QaY"/>
<constraint firstAttribute="bottom" secondItem="UsM-Al-MCC" secondAttribute="bottom" id="Eii-Wt-WAj"/>
<constraint firstItem="tvs-e8-vSU" firstAttribute="trailing" secondItem="IhK-ce-TXk" secondAttribute="trailing" id="FcR-B0-Kwb"/>
<constraint firstAttribute="trailing" secondItem="UsM-Al-MCC" secondAttribute="trailing" id="FrY-zu-zwd"/>
<constraint firstItem="UsM-Al-MCC" firstAttribute="top" secondItem="IhK-ce-TXk" secondAttribute="bottom" id="Tfo-lj-gPb"/>
<constraint firstAttribute="trailing" secondItem="ZKD-Be-eMp" secondAttribute="trailing" id="Yrh-qN-0sk"/>
<constraint firstItem="IhK-ce-TXk" firstAttribute="bottom" secondItem="ZKD-Be-eMp" secondAttribute="bottom" id="fae-mk-c1a"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="321.60000000000002" y="248.27586206896552"/>
</view>
</objects>
<resources>
<systemColor name="darkTextColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View file

@ -0,0 +1,17 @@
typedef NS_ENUM(NSUInteger, MWMButtonColoring)
{
MWMButtonColoringOther,
MWMButtonColoringBlue,
MWMButtonColoringBlack,
MWMButtonColoringWhite,
MWMButtonColoringWhiteText,
MWMButtonColoringGray,
MWMButtonColoringRed
};
@interface MWMButton : UIButton
@property (copy, nonatomic) NSString * imageName;
@property (nonatomic) MWMButtonColoring coloring;
@end

View file

@ -0,0 +1,153 @@
#import "MWMButton.h"
#import "SwiftBridge.h"
static NSString * const kDefaultPattern = @"%@_%@";
static NSString * const kHighlightedPattern = @"%@_highlighted_%@";
static NSString * const kSelectedPattern = @"%@_selected_%@";
@implementation MWMButton
- (void)setImageName:(NSString *)imageName
{
_imageName = imageName;
[self setDefaultImages];
}
// This method is overridden by MWMButtonRenderer.swift
//- (void)applyTheme
//{
// [self changeColoringToOpposite];
// [super applyTheme];
//}
- (void)setColoring:(MWMButtonColoring)coloring
{
_coloring = coloring;
[self setEnabled:self.enabled];
}
- (void)changeColoringToOpposite
{
if (self.coloring == MWMButtonColoringOther)
{
if (self.imageName)
{
[self setDefaultImages];
self.imageView.image = [self imageForState:self.state];
}
return;
}
if (self.state == UIControlStateNormal)
[self setDefaultTintColor];
else if (self.state == UIControlStateHighlighted)
[self setHighlighted:YES];
else if (self.state == UIControlStateSelected)
[self setSelected:YES];
}
- (void)setDefaultImages
{
NSString * postfix = [UIColor isNightMode] ? @"dark" : @"light";
[self setImage:[UIImage imageNamed:[NSString stringWithFormat:kDefaultPattern, self.imageName, postfix]] forState:UIControlStateNormal];
[self setImage:[UIImage imageNamed:[NSString stringWithFormat:kHighlightedPattern, self.imageName, postfix]] forState:UIControlStateHighlighted];
[self setImage:[UIImage imageNamed:[NSString stringWithFormat:kSelectedPattern, self.imageName, postfix]] forState:UIControlStateSelected];
}
- (void)setHighlighted:(BOOL)highlighted
{
[super setHighlighted:highlighted];
if (highlighted)
{
switch (self.coloring)
{
case MWMButtonColoringBlue:
self.tintColor = [UIColor linkBlueHighlighted];
break;
case MWMButtonColoringBlack:
self.tintColor = [UIColor blackHintText];
break;
case MWMButtonColoringGray:
self.tintColor = [UIColor blackDividers];
break;
case MWMButtonColoringWhiteText:
self.tintColor = [UIColor whitePrimaryTextHighlighted];
break;
case MWMButtonColoringRed:
self.tintColor = [UIColor buttonRed];
break;
case MWMButtonColoringWhite:
case MWMButtonColoringOther:
break;
}
}
else
{
if (self.selected)
[self setSelected:YES];
else
[self setEnabled:self.enabled];
}
}
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
if (selected)
{
switch (self.coloring)
{
case MWMButtonColoringBlack:
self.tintColor = [UIColor linkBlue];
break;
case MWMButtonColoringWhite:
case MWMButtonColoringWhiteText:
case MWMButtonColoringBlue:
case MWMButtonColoringOther:
case MWMButtonColoringGray:
case MWMButtonColoringRed:
break;
}
}
else
{
[self setEnabled:self.enabled];
}
}
- (void)setEnabled:(BOOL)enabled
{
[super setEnabled:enabled];
if (!enabled)
self.tintColor = [UIColor lightGrayColor];
else
[self setDefaultTintColor];
}
- (void)setDefaultTintColor
{
switch (self.coloring)
{
case MWMButtonColoringBlack:
self.tintColor = [UIColor blackSecondaryText];
break;
case MWMButtonColoringWhite:
self.tintColor = [UIColor white];
break;
case MWMButtonColoringWhiteText:
self.tintColor = [UIColor whitePrimaryText];
break;
case MWMButtonColoringBlue:
self.tintColor = [UIColor linkBlue];
break;
case MWMButtonColoringGray:
self.tintColor = [UIColor blackHintText];
break;
case MWMButtonColoringRed:
self.tintColor = [UIColor red];
break;
case MWMButtonColoringOther:
self.imageView.image = [self imageForState:UIControlStateNormal];
break;
}
}
@end

View file

@ -0,0 +1,5 @@
#import "MWMController.h"
@interface MWMCollectionViewController : UICollectionViewController<MWMController>
@end

View file

@ -0,0 +1,33 @@
#import "MWMCollectionViewController.h"
#import "MWMAlertViewController.h"
#import "MapViewController.h"
#import "SwiftBridge.h"
@interface MWMCollectionViewController ()
@property(nonatomic, readwrite) MWMAlertViewController * alertController;
@end
@implementation MWMCollectionViewController
- (BOOL)prefersStatusBarHidden { return NO; }
- (void)viewDidLoad
{
[super viewDidLoad];
self.collectionView.styleName = @"PressBackground";
[self.navigationController.navigationBar setTranslucent:NO];
}
#pragma mark - Properties
- (BOOL)hasNavigationBar { return YES; }
- (MWMAlertViewController *)alertController
{
if (!_alertController)
_alertController = [[MWMAlertViewController alloc] initWithViewController:self];
return _alertController;
}
@end

View file

@ -0,0 +1,9 @@
@class MWMAlertViewController;
@protocol MWMController <NSObject>
@property (nonatomic, readonly) BOOL hasNavigationBar;
@property (nonatomic, readonly) MWMAlertViewController * alertController;
@end

View file

@ -0,0 +1,5 @@
#import <MessageUI/MessageUI.h>
@interface MWMMailViewController : MFMailComposeViewController
@end

View file

@ -0,0 +1,18 @@
#import "MWMMailViewController.h"
@implementation MWMMailViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationBar.tintColor = UIColor.whiteColor;
self.navigationBar.barTintColor = UIColor.primary;
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
return UIStatusBarStyleLightContent;
}
- (UIViewController *)childViewControllerForStatusBarStyle { return nil; }
@end

View file

@ -0,0 +1,3 @@
@interface MWMNavigationController : UINavigationController
@end

View file

@ -0,0 +1,86 @@
#import "MWMNavigationController.h"
#import "MWMController.h"
#import "SwiftBridge.h"
#import <SafariServices/SafariServices.h>
@interface MWMNavigationController () <UINavigationControllerDelegate>
@end
@implementation MWMNavigationController
- (UIStatusBarStyle)preferredStatusBarStyle
{
return UIStatusBarStyleLightContent;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.delegate = self;
self.navigationItem.leftBarButtonItem.tintColor = [UIColor whitePrimaryText];
self.navigationItem.rightBarButtonItem.tintColor = [UIColor whitePrimaryText];
[MWMThemeManager invalidate];
}
- (void)navigationController:(UINavigationController *)navigationController
willShowViewController:(UIViewController *)viewController
animated:(BOOL)animated
{
if ([viewController isKindOfClass:[SFSafariViewController class]])
{
[navigationController setNavigationBarHidden:YES animated:animated];
return;
}
if ([viewController conformsToProtocol:@protocol(MWMController)]) {
id<MWMController> vc = (id<MWMController>)viewController;
[navigationController setNavigationBarHidden:!vc.hasNavigationBar animated:animated];
}
}
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
UIViewController * topVC = self.viewControllers.lastObject;
[self setupNavigationBackButtonItemFor:topVC];
[super pushViewController:viewController animated:animated];
}
- (void)setViewControllers:(NSArray<UIViewController *> *)viewControllers animated:(BOOL)animated {
[viewControllers enumerateObjectsUsingBlock:^(UIViewController * vc, NSUInteger idx, BOOL * stop) {
if (idx == viewControllers.count - 1)
return;
[self setupNavigationBackButtonItemFor:vc];
}];
[super setViewControllers:viewControllers animated:animated];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange: previousTraitCollection];
// Update the app theme when the device appearance is changing.
if ((self.traitCollection.verticalSizeClass != previousTraitCollection.verticalSizeClass)
|| (self.traitCollection.horizontalSizeClass != previousTraitCollection.horizontalSizeClass) || (self.traitCollection.userInterfaceStyle != previousTraitCollection.userInterfaceStyle)) {
[MWMThemeManager invalidate];
}
}
- (BOOL)shouldAutorotate
{
return YES;
}
- (void)setupNavigationBackButtonItemFor:(UIViewController *)viewController {
if (@available(iOS 14.0, *)) {
viewController.navigationItem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeMinimal;
} else {
viewController.navigationItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@""
style:UIBarButtonItemStylePlain
target:nil
action:nil];
}
}
@end

View file

@ -0,0 +1,3 @@
@interface MWMStartButton : UIButton
@end

View file

@ -0,0 +1,5 @@
#import "MWMStartButton.h"
@implementation MWMStartButton
@end

View file

@ -0,0 +1,3 @@
@interface MWMStopButton : UIButton
@end

View file

@ -0,0 +1,5 @@
#import "MWMStopButton.h"
@implementation MWMStopButton
@end

View file

@ -0,0 +1,15 @@
#import "MWMController.h"
NS_ASSUME_NONNULL_BEGIN
@interface MWMTableViewController : UITableViewController <MWMController>
@end
@interface UITableView (MWMTableViewController)
- (UITableViewCell *)dequeueDefaultCellForIndexPath:(NSIndexPath *)indexPath;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,86 @@
#import "MapsAppDelegate.h"
#import "MapViewController.h"
#import "MWMAlertViewController.h"
#import "MWMTableViewCell.h"
#import "MWMTableViewController.h"
#import "SwiftBridge.h"
static CGFloat const kMaxEstimatedTableViewCellHeight = 100.0;
@interface MWMTableViewController ()
@property (nonatomic, readwrite) MWMAlertViewController * alertController;
@end
@implementation MWMTableViewController
- (BOOL)prefersStatusBarHidden
{
return NO;
}
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
{
[self fixHeaderAndFooterFontsInDarkMode:view];
}
- (void)tableView:(UITableView *)tableView willDisplayFooterView:(UIView *)view forSection:(NSInteger)section
{
[self fixHeaderAndFooterFontsInDarkMode:view];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return kMaxEstimatedTableViewCellHeight;
}
// Fix table section header font color for all tables, including Setting and Route Options.
- (void)fixHeaderAndFooterFontsInDarkMode:(UIView*)headerView {
if ([headerView isKindOfClass: [UITableViewHeaderFooterView class]]) {
UITableViewHeaderFooterView* header = (UITableViewHeaderFooterView *)headerView;
header.textLabel.textColor = [UIColor blackSecondaryText];
if (self.tableView.style == UITableViewStyleGrouped) {
header.detailTextLabel.textColor = [UIColor blackSecondaryText];
}
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.insetsContentViewsToSafeArea = YES;
self.tableView.styleName = @"TableView:PressBackground";
[self.navigationController.navigationBar setTranslucent:NO];
[self.tableView registerClass:[MWMTableViewCell class]
forCellReuseIdentifier:[UITableViewCell className]];
[self.tableView registerClass:[MWMTableViewSubtitleCell class]
forCellReuseIdentifier:[MWMTableViewSubtitleCell className]];
}
#pragma mark - Properties
- (BOOL)hasNavigationBar
{
return YES;
}
- (MWMAlertViewController *)alertController
{
if (!_alertController)
_alertController = [[MWMAlertViewController alloc] initWithViewController:self];
return _alertController;
}
@end
@implementation UITableView (MWMTableViewController)
- (UITableViewCell *)dequeueDefaultCellForIndexPath:(NSIndexPath *)indexPath {
return [self dequeueReusableCellWithIdentifier:UITableViewCell.className forIndexPath:indexPath];
}
@end

View file

@ -0,0 +1,4 @@
#import "MWMController.h"
@interface MWMViewController : UIViewController <MWMController>
@end

View file

@ -0,0 +1,40 @@
#import "MapsAppDelegate.h"
#import "MapViewController.h"
#import "MWMAlertViewController.h"
#import "MWMViewController.h"
@interface MWMViewController ()
@property (nonatomic, readwrite) MWMAlertViewController * alertController;
@end
@implementation MWMViewController
- (BOOL)prefersStatusBarHidden
{
return NO;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self.navigationController.navigationBar setTranslucent:NO];
}
#pragma mark - Properties
- (BOOL)hasNavigationBar
{
return YES;
}
- (MWMAlertViewController *)alertController
{
if (!_alertController)
_alertController = [[MWMAlertViewController alloc] initWithViewController:self];
return _alertController;
}
@end

View file

@ -0,0 +1,27 @@
final class AlertPresentationController: DimmedModalPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
let f = super.frameOfPresentedViewInContainerView
let s = presentedViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
let r = CGRect(x: 0, y: 0, width: s.width, height: s.height)
return r.offsetBy(dx: (f.width - r.width) / 2, dy: (f.height - r.height) / 2)
}
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
presentedViewController.view.layer.setCornerRadius(.modalSheet)
presentedViewController.view.clipsToBounds = true
guard let containerView = containerView, let presentedView = presentedView else { return }
containerView.addSubview(presentedView)
presentedView.center = containerView.center
presentedView.frame = frameOfPresentedViewInContainerView
presentedView.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin, .flexibleRightMargin, .flexibleBottomMargin]
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
super.presentationTransitionDidEnd(completed)
guard let presentedView = presentedView else { return }
if completed {
presentedView.removeFromSuperview()
}
}
}

View file

@ -0,0 +1,21 @@
final class CoverVerticalDismissalAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return kDefaultAnimationDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to)
else { return }
let originFrame = transitionContext.finalFrame(for: toVC)
let finalFrame = originFrame.offsetBy(dx: 0, dy: originFrame.height)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
animations: {
fromVC.view.frame = finalFrame
}) { finished in
fromVC.view.removeFromSuperview()
transitionContext.completeTransition(finished)
}
}
}

View file

@ -0,0 +1,39 @@
final class CoverVerticalModalTransitioning: NSObject, UIViewControllerTransitioningDelegate {
private var height: CGFloat
init(presentationHeight: CGFloat) {
height = presentationHeight
}
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CoverVerticalPresentationAnimator()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CoverVerticalDismissalAnimator()
}
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting, presentationHeight: height)
}
}
fileprivate final class PresentationController: DimmedModalPresentationController {
private var height: CGFloat
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, presentationHeight: CGFloat) {
height = presentationHeight
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
required init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, cancellable: Bool = true) {
fatalError("init(presentedViewController:presenting:cancellable:) has not been implemented")
}
override var frameOfPresentedViewInContainerView: CGRect {
let f = super.frameOfPresentedViewInContainerView
return CGRect(x: 0, y: f.height - height, width: f.width, height: height)
}
}

View file

@ -0,0 +1,21 @@
final class CoverVerticalPresentationAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return kDefaultAnimationDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toVC = transitionContext.viewController(forKey: .to) else { return }
let containerView = transitionContext.containerView
let finalFrame = transitionContext.finalFrame(for: toVC)
let originFrame = finalFrame.offsetBy(dx: 0, dy: finalFrame.height)
containerView.addSubview(toVC.view)
toVC.view.frame = originFrame
toVC.view.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
UIView.animate(withDuration: transitionDuration(using: transitionContext),
animations: {
toVC.view.frame = finalFrame
}) { transitionContext.completeTransition($0) }
}
}

View file

@ -0,0 +1,50 @@
class DimmedModalPresentationController: UIPresentationController {
private lazy var onTapGr: UITapGestureRecognizer = {
return UITapGestureRecognizer(target: self, action: #selector(onTap))
}()
private lazy var dimView: UIView = {
let view = UIView()
view.setStyle(.blackStatusBarBackground)
if isCancellable {
view.addGestureRecognizer(onTapGr)
}
return view
}()
let isCancellable: Bool
required init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, cancellable: Bool = true) {
isCancellable = cancellable
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
@objc private func onTap() {
presentingViewController.dismiss(animated: true, completion: nil)
}
override func presentationTransitionWillBegin() {
guard let containerView = containerView else { return }
containerView.addSubview(dimView)
dimView.frame = containerView.bounds
dimView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
dimView.alpha = 0
presentingViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimView.alpha = 1
})
}
override func presentationTransitionDidEnd(_ completed: Bool) {
if !completed { dimView.removeFromSuperview() }
}
override func dismissalTransitionWillBegin() {
presentingViewController.transitionCoordinator?.animate(alongsideTransition: { _ in
self.dimView.alpha = 0
})
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
if completed { dimView.removeFromSuperview() }
}
}

View file

@ -0,0 +1,15 @@
final class FadeInAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return kDefaultAnimationDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let presentedView = transitionContext.view(forKey: .to) else { return }
presentedView.alpha = 0
UIView.animate(withDuration: transitionDuration(using: transitionContext),
animations: {
presentedView.alpha = 1
}) { transitionContext.completeTransition($0) }
}
}

View file

@ -0,0 +1,15 @@
final class FadeOutAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return kDefaultAnimationDuration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let presentedView = transitionContext.view(forKey: .from) else { return }
UIView.animate(withDuration: transitionDuration(using: transitionContext),
animations: {
presentedView.alpha = 0
}) { finished in
transitionContext.completeTransition(finished)
}
}
}

View file

@ -0,0 +1,26 @@
class FadeTransitioning<T: DimmedModalPresentationController>: NSObject, UIViewControllerTransitioningDelegate {
let presentedTransitioning = FadeInAnimatedTransitioning()
let dismissedTransitioning = FadeOutAnimatedTransitioning()
let isCancellable: Bool
init(cancellable: Bool = true) {
isCancellable = cancellable
super.init()
}
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return presentedTransitioning
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return dismissedTransitioning
}
func presentationController(forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController) -> UIPresentationController? {
return T(presentedViewController: presented, presenting: presenting, cancellable: isCancellable)
}
}

View file

@ -0,0 +1,34 @@
final class IPadModalPresentationController: DimmedModalPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView = containerView else { return CGRect.zero }
let screenSize = UIScreen.main.bounds
let contentSize = presentedViewController.preferredContentSize
let r = alternative(iPhone: containerView.bounds,
iPad: CGRect(x: screenSize.width/2 - contentSize.width/2,
y: screenSize.height/2 - contentSize.height/2,
width: contentSize.width,
height: contentSize.height))
return r
}
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
presentedViewController.view.layer.setCornerRadius(.buttonDefault)
presentedViewController.view.clipsToBounds = true
guard let containerView = containerView, let presentedView = presentedView else { return }
containerView.addSubview(presentedView)
presentedView.frame = frameOfPresentedViewInContainerView
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
super.presentationTransitionDidEnd(completed)
guard let presentedView = presentedView else { return }
if completed {
presentedView.removeFromSuperview()
}
}
}

View file

@ -0,0 +1,33 @@
final class PromoBookingPresentationController: DimmedModalPresentationController {
let sideMargin: CGFloat = 32.0
let maxWidth: CGFloat = 310.0
override var frameOfPresentedViewInContainerView: CGRect {
let f = super.frameOfPresentedViewInContainerView
let estimatedWidth = min(maxWidth, f.width - (sideMargin * 2.0))
let s = presentedViewController.view.systemLayoutSizeFitting(CGSize(width: estimatedWidth, height: f.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
let r = CGRect(x: (f.width - s.width) / 2, y: (f.height - s.height) / 2, width: s.width, height: s.height)
return r
}
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
presentedViewController.view.layer.setCornerRadius(.buttonDefault)
presentedViewController.view.clipsToBounds = true
guard let containerView = containerView, let presentedView = presentedView else { return }
containerView.addSubview(presentedView)
presentedView.frame = frameOfPresentedViewInContainerView
}
override func dismissalTransitionDidEnd(_ completed: Bool) {
super.presentationTransitionDidEnd(completed)
guard let presentedView = presentedView else { return }
if completed {
presentedView.removeFromSuperview()
}
}
}

View file

@ -0,0 +1,316 @@
fileprivate class ContentCell: UICollectionViewCell {
var view: UIView? {
didSet {
if let view = view, view != oldValue {
oldValue?.removeFromSuperview()
view.frame = contentView.bounds
contentView.addSubview(view)
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
if let view = view {
view.frame = contentView.bounds
}
}
}
fileprivate class HeaderCell: UICollectionViewCell {
private let label = UILabel()
private var selectedAttributes: [NSAttributedString.Key : Any] = [:]
private var deselectedAttributes: [NSAttributedString.Key : Any] = [:]
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(label)
label.textAlignment = .center
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
contentView.addSubview(label)
label.textAlignment = .center
}
override var isSelected: Bool {
didSet {
label.attributedText = NSAttributedString(string: label.text ?? "",
attributes: isSelected ? selectedAttributes : deselectedAttributes)
}
}
override func prepareForReuse() {
super.prepareForReuse()
label.attributedText = nil
}
override func layoutSubviews() {
super.layoutSubviews()
label.frame = contentView.bounds
}
func configureWith(selectedAttributes: [NSAttributedString.Key : Any],
deselectedAttributes: [NSAttributedString.Key : Any],
text: String) {
self.selectedAttributes = selectedAttributes
self.deselectedAttributes = deselectedAttributes
label.attributedText = NSAttributedString(string: text.uppercased(),
attributes: deselectedAttributes)
}
}
protocol TabViewDataSource: AnyObject {
func numberOfPages(in tabView: TabView) -> Int
func tabView(_ tabView: TabView, viewAt index: Int) -> UIView
func tabView(_ tabView: TabView, titleAt index: Int) -> String?
}
protocol TabViewDelegate: AnyObject {
func tabView(_ tabView: TabView, didSelectTabAt index: Int)
}
@objcMembers
@objc(MWMTabView)
class TabView: UIView {
private enum CellId {
static let content = "contentCell"
static let header = "headerCell"
}
private let tabsLayout = UICollectionViewFlowLayout()
private let tabsContentLayout = UICollectionViewFlowLayout()
private let tabsCollectionView: UICollectionView
private let tabsContentCollectionView: UICollectionView
private let headerView = UIView()
private let slidingView = UIView()
private var slidingViewLeft: NSLayoutConstraint!
private var slidingViewWidth: NSLayoutConstraint!
private lazy var pageCount = { return self.dataSource?.numberOfPages(in: self) ?? 0; }()
var selectedIndex: Int?
private var lastSelectedIndex: Int?
weak var dataSource: TabViewDataSource?
weak var delegate: TabViewDelegate?
var barTintColor = UIColor.white {
didSet {
headerView.backgroundColor = barTintColor
}
}
var selectedHeaderTextAttributes: [NSAttributedString.Key : Any] = [
.foregroundColor : UIColor.white,
.font : UIFont.systemFont(ofSize: 14, weight: .semibold)
] {
didSet {
tabsCollectionView.reloadData()
}
}
var deselectedHeaderTextAttributes: [NSAttributedString.Key : Any] = [
.foregroundColor : UIColor.gray,
.font : UIFont.systemFont(ofSize: 14, weight: .semibold)
] {
didSet {
tabsCollectionView.reloadData()
}
}
var contentFrame: CGRect {
safeAreaLayoutGuide.layoutFrame
}
override var tintColor: UIColor! {
didSet {
slidingView.backgroundColor = tintColor
}
}
override init(frame: CGRect) {
tabsCollectionView = UICollectionView(frame: .zero, collectionViewLayout: tabsLayout)
tabsContentCollectionView = UICollectionView(frame: .zero, collectionViewLayout: tabsContentLayout)
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
tabsCollectionView = UICollectionView(frame: .zero, collectionViewLayout: tabsLayout)
tabsContentCollectionView = UICollectionView(frame: .zero, collectionViewLayout: tabsContentLayout)
super.init(coder: aDecoder)
configure()
}
private func configure() {
backgroundColor = .white
configureHeader()
configureContent()
addSubview(tabsContentCollectionView)
addSubview(headerView)
configureLayoutContraints()
}
private func configureHeader() {
tabsLayout.scrollDirection = .horizontal
tabsLayout.minimumLineSpacing = 0
tabsLayout.minimumInteritemSpacing = 0
tabsCollectionView.register(HeaderCell.self, forCellWithReuseIdentifier: CellId.header)
tabsCollectionView.dataSource = self
tabsCollectionView.delegate = self
tabsCollectionView.backgroundColor = .clear
slidingView.backgroundColor = tintColor
headerView.backgroundColor = barTintColor
headerView.addSubview(tabsCollectionView)
headerView.addSubview(slidingView)
headerView.addSeparator(.bottom)
}
private func configureContent() {
tabsContentLayout.scrollDirection = .horizontal
tabsContentLayout.minimumLineSpacing = 0
tabsContentLayout.minimumInteritemSpacing = 0
tabsContentCollectionView.register(ContentCell.self, forCellWithReuseIdentifier: CellId.content)
tabsContentCollectionView.dataSource = self
tabsContentCollectionView.delegate = self
tabsContentCollectionView.isPagingEnabled = true
tabsContentCollectionView.bounces = false
tabsContentCollectionView.showsVerticalScrollIndicator = false
tabsContentCollectionView.showsHorizontalScrollIndicator = false
tabsContentCollectionView.backgroundColor = .clear
}
private func configureLayoutContraints() {
tabsCollectionView.translatesAutoresizingMaskIntoConstraints = false;
tabsContentCollectionView.translatesAutoresizingMaskIntoConstraints = false
headerView.translatesAutoresizingMaskIntoConstraints = false
slidingView.translatesAutoresizingMaskIntoConstraints = false
headerView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
headerView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
headerView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor).isActive = true
headerView.heightAnchor.constraint(equalToConstant: 46).isActive = true
tabsContentCollectionView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor).isActive = true
tabsContentCollectionView.rightAnchor.constraint(equalTo: safeAreaLayoutGuide.rightAnchor).isActive = true
tabsContentCollectionView.topAnchor.constraint(equalTo: headerView.bottomAnchor).isActive = true
tabsContentCollectionView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor).isActive = true
tabsCollectionView.leftAnchor.constraint(equalTo: headerView.leftAnchor).isActive = true
tabsCollectionView.rightAnchor.constraint(equalTo: headerView.rightAnchor).isActive = true
tabsCollectionView.topAnchor.constraint(equalTo: headerView.topAnchor).isActive = true
tabsCollectionView.bottomAnchor.constraint(equalTo: slidingView.topAnchor).isActive = true
slidingView.heightAnchor.constraint(equalToConstant: 3).isActive = true
slidingView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor).isActive = true
slidingViewLeft = slidingView.leftAnchor.constraint(equalTo: safeAreaLayoutGuide.leftAnchor)
slidingViewLeft.isActive = true
slidingViewWidth = slidingView.widthAnchor.constraint(equalToConstant: 0)
slidingViewWidth.isActive = true
}
override func layoutSubviews() {
tabsLayout.invalidateLayout()
tabsContentLayout.invalidateLayout()
super.layoutSubviews()
assert(pageCount > 0)
slidingViewWidth.constant = pageCount > 0 ? contentFrame.width / CGFloat(pageCount) : 0
slidingViewLeft.constant = pageCount > 0 ? contentFrame.width / CGFloat(pageCount) * CGFloat(selectedIndex ?? 0) : 0
tabsCollectionView.layoutIfNeeded()
tabsContentCollectionView.layoutIfNeeded()
if let selectedIndex = selectedIndex {
tabsContentCollectionView.scrollToItem(at: IndexPath(item: selectedIndex, section: 0),
at: .left,
animated: false)
}
}
}
extension TabView : UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return pageCount
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell = UICollectionViewCell()
if collectionView == tabsContentCollectionView {
cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellId.content, for: indexPath)
if let contentCell = cell as? ContentCell {
contentCell.view = dataSource?.tabView(self, viewAt: indexPath.item)
}
}
if collectionView == tabsCollectionView {
cell = collectionView.dequeueReusableCell(withReuseIdentifier: CellId.header, for: indexPath)
if let headerCell = cell as? HeaderCell {
let title = dataSource?.tabView(self, titleAt: indexPath.item) ?? ""
headerCell.configureWith(selectedAttributes: selectedHeaderTextAttributes,
deselectedAttributes: deselectedHeaderTextAttributes,
text: title)
if indexPath.item == selectedIndex {
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: [])
}
}
}
return cell
}
}
extension TabView : UICollectionViewDelegateFlowLayout {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentSize.width > 0 {
let scrollOffset = scrollView.contentOffset.x / scrollView.contentSize.width
slidingViewLeft.constant = scrollOffset * contentFrame.width
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
lastSelectedIndex = selectedIndex
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
selectedIndex = Int(round(scrollView.contentOffset.x / scrollView.bounds.width))
if let selectedIndex = selectedIndex, selectedIndex != lastSelectedIndex {
delegate?.tabView(self, didSelectTabAt: selectedIndex)
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if (collectionView == tabsCollectionView) {
let isSelected = selectedIndex == indexPath.item
if !isSelected {
selectedIndex = indexPath.item
tabsContentCollectionView.scrollToItem(at: indexPath, at: .left, animated: true)
delegate?.tabView(self, didSelectTabAt: selectedIndex!)
}
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if (collectionView == tabsCollectionView) {
collectionView.deselectItem(at: indexPath, animated: false)
}
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let bounds = collectionView.bounds.inset(by: collectionView.adjustedContentInset)
if collectionView == tabsContentCollectionView {
return bounds.size
} else {
return CGSize(width: bounds.width / CGFloat(pageCount),
height: bounds.height)
}
}
}

View file

@ -0,0 +1,36 @@
class TabViewController: MWMViewController {
var viewControllers: [UIViewController] = [] {
didSet {
viewControllers.forEach {
self.addChild($0)
$0.didMove(toParent: self)
}
}
}
var tabView: TabView {
get {
return view as! TabView
}
}
override func loadView() {
let v = TabView()
v.dataSource = self
view = v
}
}
extension TabViewController: TabViewDataSource {
func numberOfPages(in tabView: TabView) -> Int {
return viewControllers.count
}
func tabView(_ tabView: TabView, viewAt index: Int) -> UIView {
return viewControllers[index].view
}
func tabView(_ tabView: TabView, titleAt index: Int) -> String? {
return viewControllers[index].title
}
}

View file

@ -0,0 +1,88 @@
@IBDesignable
class VerticallyAlignedButton: UIControl {
@IBInspectable
var image: UIImage? {
didSet {
imageView.image = image
}
}
@IBInspectable
var title: String? {
didSet {
if localizedText == nil {
titleLabel.text = title
}
}
}
@IBInspectable
var localizedText: String? {
didSet {
if let localizedText = localizedText {
titleLabel.text = L(localizedText)
}
}
}
@IBInspectable
var spacing: CGFloat = 4 {
didSet {
spacingConstraint.constant = spacing
}
}
@IBInspectable
var numberOfLines: Int {
get {
return titleLabel.numberOfLines
}
set {
titleLabel.numberOfLines = newValue
}
}
private lazy var spacingConstraint: NSLayoutConstraint = {
let spacingConstraint = titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: spacing)
return spacingConstraint
}()
lazy var titleLabel: UILabel = {
let titleLabel = UILabel()
titleLabel.textAlignment = .center
titleLabel.translatesAutoresizingMaskIntoConstraints = false
return titleLabel
}()
lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
private func setupView() {
addSubview(titleLabel)
addSubview(imageView)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: topAnchor),
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor),
spacingConstraint,
titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
titleLabel.topAnchor.constraint(equalTo: bottomAnchor)
])
}
}