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,40 @@
#import "MWMButton.h"
#import "MWMCircularProgressState.h"
#import "UIImageView+Coloring.h"
NS_ASSUME_NONNULL_BEGIN
@class MWMCircularProgress;
typedef NSArray<NSNumber *> *MWMCircularProgressStateVec;
@protocol MWMCircularProgressProtocol<NSObject>
- (void)progressButtonPressed:(MWMCircularProgress *)progress;
@end
@interface MWMCircularProgress: NSObject<CAAnimationDelegate>
+ (instancetype)downloaderProgressForParentView:(UIView *)parentView;
@property(nonatomic) CGFloat progress;
@property(nonatomic) MWMCircularProgressState state;
@property(weak, nonatomic, nullable) id<MWMCircularProgressProtocol> delegate;
- (void)setSpinnerColoring:(MWMImageColoring)coloring;
- (void)setSpinnerBackgroundColor:(UIColor *)backgroundColor;
- (void)setInvertColor:(BOOL)invertColor;
- (void)setCancelButtonHidden;
- (nonnull instancetype)init __attribute__((unavailable("init is not available")));
- (nonnull instancetype)initWithParentView:(UIView *)parentView;
- (void)setImageName:(nullable NSString *)imageName
forStates:(MWMCircularProgressStateVec)states;
- (void)setColor:(UIColor *)color forStates:(MWMCircularProgressStateVec)states;
- (void)setColoring:(MWMButtonColoring)coloring
forStates:(MWMCircularProgressStateVec)states;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,152 @@
#import "MWMCircularProgress.h"
#import "MWMCircularProgressView.h"
@interface MWMCircularProgressView ()
@property(nonatomic) BOOL suspendRefreshProgress;
@end
@interface MWMCircularProgress ()
@property(nonatomic) IBOutlet MWMCircularProgressView * rootView;
@property(nonatomic) NSNumber * nextProgressToAnimate;
@end
@implementation MWMCircularProgress
+ (nonnull instancetype)downloaderProgressForParentView:(nonnull UIView *)parentView
{
MWMCircularProgress * progress = [[self alloc] initWithParentView:parentView];
progress.rootView.suspendRefreshProgress = YES;
[progress setImageName:@"ic_download"
forStates:@[@(MWMCircularProgressStateNormal), @(MWMCircularProgressStateSelected)]];
[progress setImageName:@"ic_close_spinner"
forStates:@[@(MWMCircularProgressStateProgress), @(MWMCircularProgressStateSpinner)]];
[progress setImageName:@"ic_download_error" forStates:@[@(MWMCircularProgressStateFailed)]];
[progress setImageName:@"ic_check" forStates:@[@(MWMCircularProgressStateCompleted)]];
[progress setColoring:MWMButtonColoringBlack
forStates:@[@(MWMCircularProgressStateNormal), @(MWMCircularProgressStateSelected),
@(MWMCircularProgressStateProgress), @(MWMCircularProgressStateSpinner)]];
[progress setColoring:MWMButtonColoringOther forStates:@[@(MWMCircularProgressStateFailed)]];
[progress setColoring:MWMButtonColoringBlue forStates:@[@(MWMCircularProgressStateCompleted)]];
progress.rootView.suspendRefreshProgress = NO;
return progress;
}
- (nonnull instancetype)initWithParentView:(nonnull UIView *)parentView
{
self = [super init];
if (self)
{
[[UINib nibWithNibName:NSStringFromClass([self class]) bundle:nil] instantiateWithOwner:self options:nil];
[parentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
[parentView addSubview:self.rootView];
self.rootView.translatesAutoresizingMaskIntoConstraints = NO;
[self.rootView.topAnchor constraintEqualToAnchor:parentView.topAnchor].active = YES;
[self.rootView.bottomAnchor constraintEqualToAnchor:parentView.bottomAnchor].active = YES;
[self.rootView.leadingAnchor constraintEqualToAnchor:parentView.leadingAnchor].active = YES;
[self.rootView.trailingAnchor constraintEqualToAnchor:parentView.trailingAnchor].active = YES;
self.state = MWMCircularProgressStateNormal;
}
return self;
}
- (void)dealloc { [self.rootView removeFromSuperview]; }
- (void)reset
{
_progress = 0.;
[self.rootView updatePath:0.];
self.nextProgressToAnimate = nil;
}
- (void)setSpinnerColoring:(MWMImageColoring)coloring
{
[self.rootView setSpinnerColoring:coloring];
}
- (void)setSpinnerBackgroundColor:(nonnull UIColor *)backgroundColor
{
[self.rootView setSpinnerBackgroundColor:backgroundColor];
}
- (void)setImageName:(nullable NSString *)imageName
forStates:(MWMCircularProgressStateVec)states
{
for (NSNumber *state in states)
[self.rootView setImageName:imageName forState:(MWMCircularProgressState)state.integerValue];
}
- (void)setColor:(UIColor *)color forStates:(MWMCircularProgressStateVec)states
{
for (NSNumber *state in states)
[self.rootView setColor:color forState:(MWMCircularProgressState)state.integerValue];
}
- (void)setColoring:(MWMButtonColoring)coloring
forStates:(MWMCircularProgressStateVec)states
{
for (NSNumber *state in states)
[self.rootView setColoring:coloring forState:(MWMCircularProgressState)state.integerValue];
}
- (void)setInvertColor:(BOOL)invertColor { self.rootView.isInvertColor = invertColor; }
#pragma mark - Animation
- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag
{
if (self.nextProgressToAnimate)
{
self.progress = self.nextProgressToAnimate.doubleValue;
self.nextProgressToAnimate = nil;
}
}
#pragma mark - Actions
- (void)setCancelButtonHidden
{
self.rootView.buttonView.hidden = YES;
}
- (IBAction)buttonTouchUpInside:(UIButton *)sender { [self.delegate progressButtonPressed:self]; }
#pragma mark - Properties
- (void)setProgress:(CGFloat)progress
{
if (progress < _progress || progress <= 0)
{
self.state = MWMCircularProgressStateSpinner;
return;
}
self.rootView.state = MWMCircularProgressStateProgress;
if (self.rootView.animating)
{
if (progress > self.nextProgressToAnimate.floatValue)
self.nextProgressToAnimate = @(progress);
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.rootView animateFromValue:self->_progress toValue:progress];
self->_progress = progress;
});
}
}
- (void)setState:(MWMCircularProgressState)state
{
dispatch_async(dispatch_get_main_queue(), ^{
[self reset];
self.rootView.state = state;
});
}
- (MWMCircularProgressState)state { return self.rootView.state; }
@end

View file

@ -0,0 +1,64 @@
<?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" customClass="MWMCircularProgress">
<connections>
<outlet property="rootView" destination="2DE-Qh-89K" id="1DR-Lj-Wv9"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="2DE-Qh-89K" customClass="MWMCircularProgressView" propertyAccessControl="none">
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" adjustsImageWhenDisabled="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="viF-Ee-7h0" customClass="MWMButton">
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<state key="normal">
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="string" keyPath="styleName" value="MWMBlack"/>
</userDefinedRuntimeAttributes>
<connections>
<action selector="buttonTouchUpInside:" destination="-1" eventType="touchUpInside" id="aPG-4e-uSw"/>
</connections>
</button>
<imageView hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="aMt-XV-9UK" userLabel="Spinner">
<rect key="frame" x="0.0" y="0.0" width="32" height="32"/>
</imageView>
</subviews>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="viF-Ee-7h0" firstAttribute="top" secondItem="2DE-Qh-89K" secondAttribute="top" id="3gS-24-Olj"/>
<constraint firstItem="aMt-XV-9UK" firstAttribute="width" relation="lessThanOrEqual" secondItem="vnA-dQ-QpI" secondAttribute="width" id="Bxj-4M-UlX"/>
<constraint firstItem="aMt-XV-9UK" firstAttribute="centerX" secondItem="vnA-dQ-QpI" secondAttribute="centerX" id="EJm-Be-grG"/>
<constraint firstItem="aMt-XV-9UK" firstAttribute="leading" secondItem="2DE-Qh-89K" secondAttribute="leading" priority="100" id="JK6-QX-JcI"/>
<constraint firstAttribute="bottom" secondItem="aMt-XV-9UK" secondAttribute="bottom" priority="100" id="VFI-Yh-q1q"/>
<constraint firstItem="aMt-XV-9UK" firstAttribute="centerY" secondItem="vnA-dQ-QpI" secondAttribute="centerY" id="eVP-mP-bGc"/>
<constraint firstItem="aMt-XV-9UK" firstAttribute="height" relation="lessThanOrEqual" secondItem="vnA-dQ-QpI" secondAttribute="height" id="eWY-kL-sQD"/>
<constraint firstAttribute="trailing" secondItem="viF-Ee-7h0" secondAttribute="trailing" id="fQT-G3-rJF"/>
<constraint firstItem="aMt-XV-9UK" firstAttribute="top" secondItem="2DE-Qh-89K" secondAttribute="top" priority="100" id="fbs-bw-DIs"/>
<constraint firstAttribute="bottom" secondItem="viF-Ee-7h0" secondAttribute="bottom" id="j4s-7V-lg9"/>
<constraint firstAttribute="trailing" secondItem="aMt-XV-9UK" secondAttribute="trailing" priority="100" id="k01-wU-25n"/>
<constraint firstItem="viF-Ee-7h0" firstAttribute="leading" secondItem="2DE-Qh-89K" secondAttribute="leading" id="sHS-iu-Url"/>
</constraints>
<nil key="simulatedStatusBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<viewLayoutGuide key="safeArea" id="vnA-dQ-QpI"/>
<connections>
<outlet property="button" destination="viF-Ee-7h0" id="0zP-Ye-sRs"/>
<outlet property="owner" destination="-1" id="0n7-PI-5tO"/>
<outlet property="spinner" destination="aMt-XV-9UK" id="bmp-bV-h3C"/>
</connections>
<point key="canvasLocation" x="139" y="155"/>
</view>
</objects>
</document>

View file

@ -0,0 +1,8 @@
typedef NS_ENUM(NSInteger, MWMCircularProgressState) {
MWMCircularProgressStateNormal,
MWMCircularProgressStateSelected,
MWMCircularProgressStateProgress,
MWMCircularProgressStateSpinner,
MWMCircularProgressStateFailed,
MWMCircularProgressStateCompleted
};

View file

@ -0,0 +1,27 @@
#import "MWMButton.h"
#import "MWMCircularProgress.h"
@interface MWMCircularProgressView : UIView
@property(nonatomic, readonly) BOOL animating;
@property(nonatomic) MWMCircularProgressState state;
@property(nonatomic) BOOL isInvertColor;
- (nonnull instancetype)initWithFrame:(CGRect)frame
__attribute__((unavailable("initWithFrame is not available")));
- (nonnull instancetype)init __attribute__((unavailable("init is not available")));
- (void)setSpinnerColoring:(MWMImageColoring)coloring;
- (void)setSpinnerBackgroundColor:(nonnull UIColor *)backgroundColor;
- (void)setImageName:(nullable NSString *)imageName forState:(MWMCircularProgressState)state;
- (void)setColor:(nonnull UIColor *)color forState:(MWMCircularProgressState)state;
- (void)setColoring:(MWMButtonColoring)coloring forState:(MWMCircularProgressState)state;
- (void)animateFromValue:(CGFloat)fromValue toValue:(CGFloat)toValue;
- (void)updatePath:(CGFloat)progress;
- (UIView * _Nullable)buttonView;
@end

View file

@ -0,0 +1,261 @@
#import "MWMCircularProgressView.h"
#import "SwiftBridge.h"
#import "UIImageView+Coloring.h"
static CGFloat const kLineWidth = 2.0;
static NSString * const kAnimationKey = @"CircleAnimation";
static CGFloat angleWithProgress(CGFloat progress) { return 2.0 * M_PI * progress - M_PI_2; }
@interface MWMCircularProgressView ()
@property(nonatomic) CAShapeLayer * backgroundLayer;
@property(nonatomic) CAShapeLayer * progressLayer;
@property(nonatomic) UIColor * spinnerBackgroundColor;
@property(nonatomic, readonly) CGColorRef progressLayerColor;
@property(nonatomic) NSMutableDictionary * colors;
@property(nonatomic) NSMutableDictionary<NSNumber *, NSNumber *> * buttonColoring;
@property(nonatomic) NSMutableDictionary<NSNumber *, NSString *> * images;
@property(weak, nonatomic) IBOutlet MWMCircularProgress * owner;
@property(weak, nonatomic) IBOutlet UIImageView * spinner;
@property(weak, nonatomic) IBOutlet MWMButton * button;
@property(nonatomic) BOOL suspendRefreshProgress;
@end
@implementation MWMCircularProgressView
- (void)awakeFromNib
{
[super awakeFromNib];
self.suspendRefreshProgress = YES;
[self setupColors];
[self setupButtonColoring];
self.images = [NSMutableDictionary dictionary];
[self setupAnimationLayers];
self.suspendRefreshProgress = NO;
}
#pragma mark - Setup
- (void)setupColors
{
self.colors = [NSMutableDictionary dictionary];
UIColor * progressColor = [_spinnerBackgroundColor isEqual:UIColor.clearColor]
? UIColor.whiteColor
: [UIColor linkBlue];
UIColor * clearColor = UIColor.clearColor;
[self setSpinnerColoring:MWMImageColoringGray];
[self setColor:clearColor forState:MWMCircularProgressStateNormal];
[self setColor:clearColor forState:MWMCircularProgressStateSelected];
[self setColor:progressColor forState:MWMCircularProgressStateProgress];
[self setColor:progressColor forState:MWMCircularProgressStateSpinner];
[self setColor:clearColor forState:MWMCircularProgressStateFailed];
[self setColor:clearColor forState:MWMCircularProgressStateCompleted];
}
- (void)setupButtonColoring
{
self.buttonColoring = [NSMutableDictionary dictionary];
[self setColoring:MWMButtonColoringBlack forState:MWMCircularProgressStateNormal];
[self setColoring:MWMButtonColoringBlue forState:MWMCircularProgressStateSelected];
[self setColoring:MWMButtonColoringBlue forState:MWMCircularProgressStateProgress];
[self setColoring:MWMButtonColoringBlue forState:MWMCircularProgressStateSpinner];
[self setColoring:MWMButtonColoringBlue forState:MWMCircularProgressStateFailed];
[self setColoring:MWMButtonColoringBlue forState:MWMCircularProgressStateCompleted];
}
- (void)applyTheme
{
[super applyTheme];
self.suspendRefreshProgress = YES;
[self setupColors];
self.suspendRefreshProgress = NO;
}
- (void)setupAnimationLayers
{
self.backgroundLayer = [CAShapeLayer layer];
self.progressLayer = [CAShapeLayer layer];
[self refreshProgress];
[self.layer addSublayer:self.backgroundLayer];
[self.layer addSublayer:self.progressLayer];
}
- (void)setSpinnerColoring:(MWMImageColoring)coloring { self.spinner.mwm_coloring = coloring; }
- (void)setImageName:(nullable NSString *)imageName forState:(MWMCircularProgressState)state
{
self.images[@(state)] = imageName;
[self refreshProgress];
}
- (void)setColor:(UIColor *)color forState:(MWMCircularProgressState)state
{
self.colors[@(state)] = color;
[self refreshProgress];
}
- (void)setColoring:(MWMButtonColoring)coloring forState:(MWMCircularProgressState)state
{
self.buttonColoring[@(state)] = @(coloring);
[self refreshProgress];
}
#pragma mark - Progress
- (void)refreshProgress
{
if (self.suspendRefreshProgress)
return;
self.backgroundLayer.fillColor = self.progressLayer.fillColor = UIColor.clearColor.CGColor;
self.backgroundLayer.lineWidth = self.progressLayer.lineWidth = kLineWidth;
self.backgroundLayer.strokeColor = self.spinnerBackgroundColor.CGColor;
[self updateBackgroundPath];
self.progressLayer.strokeColor = self.progressLayerColor;
NSString * imageName = self.images[@(self.state)];
if (imageName)
{
[self.button setImage:[UIImage imageNamed:imageName] forState:UIControlStateNormal];
UIImage *hl = [UIImage imageNamed:[imageName stringByAppendingString:@"_highlighted"]];
if (hl)
[self.button setImage:hl forState:UIControlStateHighlighted];
}
else
{
[self.button setImage:nil forState:UIControlStateNormal];
[self.button setImage:nil forState:UIControlStateHighlighted];
}
self.button.coloring = (MWMButtonColoring)self.buttonColoring[@(self.state)].unsignedIntegerValue;
}
- (void)updatePath:(CGFloat)progress
{
[self updateBackgroundPath];
if (progress > 0.0)
{
self.state =
progress < 1.0 ? MWMCircularProgressStateProgress : MWMCircularProgressStateCompleted;
[self stopSpinner];
}
self.progressLayer.path = [self pathWithProgress:progress].CGPath;
}
- (void)updateBackgroundPath
{
self.backgroundLayer.path = [self pathWithProgress:1.0].CGPath;
}
- (UIBezierPath *)pathWithProgress:(CGFloat)progress
{
CGPoint center = CGPointMake(self.width / 2.0, self.height / 2.0);
CGFloat radius = MIN(center.x, center.y) - kLineWidth;
UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:angleWithProgress(0.0)
endAngle:angleWithProgress(progress)
clockwise:YES];
return path;
}
#pragma mark - Spinner
- (void)startSpinner
{
if (self.spinner.hidden)
{
self.spinner.hidden = NO;
self.backgroundLayer.hidden = self.progressLayer.hidden = YES;
}
NSString * postfix = ([UIColor isNightMode] && !self.isInvertColor) ||
(![UIColor isNightMode] && self.isInvertColor) ||
_spinnerBackgroundColor
? @"dark"
: @"light";
UIImage * image = [UIImage imageNamed:[NSString stringWithFormat:@"Spinner_%@", postfix]];
self.spinner.image = image;
[self.spinner startRotation:1];
}
- (void)stopSpinner
{
if (self.spinner.hidden)
return;
self.spinner.hidden = YES;
self.backgroundLayer.hidden = self.progressLayer.hidden = NO;
[self.spinner stopRotation];
}
#pragma mark - Animation
- (void)animateFromValue:(CGFloat)fromValue toValue:(CGFloat)toValue
{
[self updatePath:toValue];
CABasicAnimation * animation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
animation.duration = kDefaultAnimationDuration;
animation.repeatCount = 1;
animation.fromValue = @(fromValue / toValue);
animation.toValue = @1;
animation.timingFunction =
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.delegate = self.owner;
[self.progressLayer addAnimation:animation forKey:kAnimationKey];
}
#pragma mark - Properties
- (UIView * _Nullable)buttonView
{
return self.button;
}
- (void)setState:(MWMCircularProgressState)state
{
if (state == MWMCircularProgressStateSpinner)
[self startSpinner];
else if (_state == MWMCircularProgressStateSpinner)
[self stopSpinner];
if (_state == state)
return;
_state = state;
[self refreshProgress];
}
- (UIColor *)spinnerBackgroundColor
{
if (_spinnerBackgroundColor)
return _spinnerBackgroundColor;
switch (self.state)
{
case MWMCircularProgressStateProgress: return [UIColor pressBackground];
default: return UIColor.clearColor;
}
}
- (CGColorRef)progressLayerColor
{
UIColor * color = self.colors[@(self.state)];
return color.CGColor;
}
- (void)setFrame:(CGRect)frame
{
BOOL needrefreshProgress = !CGRectEqualToRect(self.frame, frame);
super.frame = frame;
if (needrefreshProgress)
[self refreshProgress];
}
- (BOOL)animating { return [self.progressLayer animationForKey:kAnimationKey] != nil; }
- (void)setSuspendRefreshProgress:(BOOL)suspendRefreshProgress
{
_suspendRefreshProgress = suspendRefreshProgress;
if (!suspendRefreshProgress)
[self refreshProgress];
}
@end