タイミング用のカスタムUI要素の実装。 パート1

11月17日、モスクワで、モバイル開発者MBLTdevに関する国際会議の一環として Alexander Ziminが「UIKitの標準コンポーネントを超えた視覚化」に関するプレゼンテーションを行いました。 まず、このレポートは、カスタムUI要素の開発について詳しく知りたいiOS開発者を対象としています。 彼は、カスタムコントロールの例に興味があり、レポートに記載されている点を考慮して実装し、改良することにしました。 この例はSwiftで実装されましたが、 Objective-C実装しています。

カスタムUI要素を開発する方法:



実装されるもの


このレポートは、 UIPickerView似たカスタムUIView例でした。 タイミングを目的としています。



このコンポーネントはUIPickerView似ていUIPickerView 。 したがって、以下を実装する必要があります。


実装方法


UIView取り、それを丸くして、そのUILabelに数字のあるUILabelUILabelします。 回転するには、無限のcontentSizeを持つUIScrollViewを追加し、シフトに基づいて回転角度を考慮します。



必要です:


階層の準備


AYNCircleView作成します。 これは、カスタム要素全体を含むクラスになります。 この段階では、彼には何も公開されていません。私たちはすべて個人的に行っています。 次に、階層の作成を開始します。 まず、 Interface Builder viewを作成します。 AYNCircleView.xibを作成して、階層を処理しましょう。



階層は次の要素で構成されます。


constraints設定しましょう。 私たちが最も興味を持っているのはcontentViewbottom space高さです。 サークルのサイズと位置を提供します。 残りのconstraintsは、 contentViewsuperviewを超えてsuperviewを防ぎます。 便宜上、 scrollView contentSize側を定数でscrollViewます。 これはパフォーマンスに大きな影響を与えませんが、回転の「無限」をシミュレートします。 ささいなことに気をscrollViewば、「ジャンプ」システムを実装してscrollViewcontentSizeを大幅に減らすことがscrollViewます。

AYNCircleViewクラスを作成します。

 @interface AYNCircleView : UIView @end static CGFloat const kAYNCircleViewScrollViewContentSizeLength = 1000000000; @interface AYNCircleView () @property (assign, nonatomic) BOOL isInitialized; @property (assign, nonatomic) CGFloat circleRadius; @property (weak, nonatomic) IBOutlet UIView *contentView; @property (weak, nonatomic) IBOutlet UIScrollView *scrollView; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *contentViewDimension; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *contentViewOffset; @end 

Interface Builderおよびコードでviewが初期化される場合に備えて、イニシャライザーを再定義します。

 @implementation AYNCircleView #pragma mark - Initializers - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self commonInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self commonInit]; } return self; } #pragma mark - Private - (void)commonInit { UIView *nibView = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil].firstObject; [self addSubview:nibView]; self.scrollView.contentSize = CGSizeMake(kAYNCircleViewScrollViewContentSizeLength, kAYNCircleViewScrollViewContentSizeLength); self.scrollView.contentOffset = CGPointMake(kAYNCircleViewScrollViewContentSizeLength / 2.0, kAYNCircleViewScrollViewContentSizeLength / 2.0); self.scrollView.delegate = self; } 

階層を配置します。 現時点ではビューの実際のサイズがわからないため、これはイニシャライザーでは実行できません。 - (void)layoutSubviewsでそれらを見つけることができるので、そこでサイズを調整します。 これを行うには、最小の幅と高さに依存する円の半径を入力します。

 @property (assign, nonatomic) CGFloat circleRadius; 

初期化が完了したことを示すフラグを入力します。

 @property (assign, nonatomic) BOOL isInitialized; 

スクロールすると- (void)layoutSubviewsれるため、階層の位置を常に計算するのは間違っています。 views正しいサイズを設定するために制約を更新しviews

 #pragma mark - Layout - (void)layoutSubviews { [super layoutSubviews]; if (!self.isInitialized) { self.isInitialized = YES; self.subviews.firstObject.frame = self.bounds; self.circleRadius = MIN(CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds)) / 2; self.contentView.layer.cornerRadius = self.circleRadius; self.contentView.layer.masksToBounds = YES; [self setNeedsUpdateConstraints]; } } - (void)updateConstraints { self.contentViewDimension.constant = self.circleRadius * 2; self.contentViewOffset.constant = self.circleRadius; [super updateConstraints]; } 

できた 階層を構築した結果を確認します。 コントロールを配置するview controllerを作成します。



生きている階層を見てみましょう。



階層が正しく構築されました、続行します。

背景UIView


次のステップ: backgroundViewサポートします。 カスタムコントロールは、任意のviewを背景に配置できるように考案されており、このコントロールのユーザーは実装について考えません。

backgroundViewに関する情報を含むパブリックプロパティを作成しbackgroundView

 @property (strong, nonatomic) UIView *backgroundView; 

次に、階層に追加する方法を定義します。 setter再定義します。

 - (void)setBackgroundView:(UIView *)backgroundView { [_backgroundView removeFromSuperview]; _backgroundView = backgroundView; [_contentView insertSubview:_backgroundView atIndex:0]; if (_isInitialized) { [self layoutBackgroundView]; } } 

ここのロジックは何ですか? 階層から前のviewを削除し、階層の最下位レベルに新しいbackgroundViewを追加し、メソッドでそのサイズを変更します。

 - (void)layoutBackgroundView { self.backgroundView.frame = CGRectMake(0, 0, self.circleRadius * 2, self.circleRadius * 2); self.backgroundView.layer.masksToBounds = YES; self.backgroundView.layer.cornerRadius = self.circleRadius; } 

view作成さviewだけの場合も考慮してください。 サイズを正しく変更するには、このメソッドへの呼び出しを- (void)layoutSubviewsます。

新しい階層を検討してください。 赤いUIViewを追加して、階層を確認します。

 UIView *redView = [UIView new]; redView.backgroundColor = [UIColor redColor]; self.circleView.backgroundView = redView; 



すべて順調です!

ダイヤル実装


ダイヤルを実装するには、 UILabel使用しUILabel 。 生産性を高める必要がある場合は、 CoreGraphicsのレベルCoreGraphics下げて、すでに署名を追加します。 私たちのソリューションは、「回転」 labelを定義するUILabel上のカテゴリlabel 。 メソッドに少しカスタマイズを追加しました:テキストの色とフォント。

 @interface UILabel (AYNHelpers) + (UILabel *)ayn_rotatedLabelWithText:(NSString *)text angle:(CGFloat)angle circleRadius:(CGFloat)circleRadius offset:(CGFloat)offset font:(UIFont *)font textColor:(UIColor *)textColor; @end 

このメソッドにより、 labelを円に配置できます。 circleRadiusはこの円の半径を定義し、 offsetはこの円に対するoffset決定します。 angleは中心角です。 この円の中心に回転labelを作成し、 xOffsetyOffset使用して、このlabelの中心を目的の位置に移動します。

 #import "UILabel+AYNHelpers.h" @implementation UILabel (AYNHelpers) + (UILabel *)ayn_rotatedLabelWithText:(NSString *)text angle:(CGFloat)angle circleRadius:(CGFloat)circleRadius offset:(CGFloat)offset font:(UIFont *)font textColor:(UIColor *)textColor { UILabel *rotatedLabel = [[UILabel alloc] initWithFrame:CGRectZero]; rotatedLabel.text = text; rotatedLabel.font = font ?: [UIFont boldSystemFontOfSize:22.0]; rotatedLabel.textColor = textColor ?: [UIColor blackColor]; [rotatedLabel sizeToFit]; rotatedLabel.transform = CGAffineTransformMakeRotation(angle); CGFloat angleForPoint = M_PI - angle; CGFloat xOffset = sin(angleForPoint) * (circleRadius - offset); CGFloat yOffset = cos(angleForPoint) * (circleRadius - offset); rotatedLabel.center = CGPointMake(circleRadius + xOffset, circleRadius + yOffset); return rotatedLabel; } @end 

できた 次に、メソッド- (void)addLabelsWithNumber:contentViewラベルに追加する必要があります。 これを行うには、署名が配置されている角度ステップを保存すると便利です。 360度の円と12個の署名を取る場合、ステップは360/12 = 30度になります。 プロパティを作成します。回転角度を正規化すると便利です。

 @property (assign, nonatomic) CGFloat angleStep;   offset  ,    . static CGFloat const kAYNCircleViewLabelOffset = 10; 

ラベルに対して一定のoffsetを作成しますが、これも後で必要になります。

 - (void)addLabelsWithNumber:(NSInteger)numberOfLabels { if (numberOfLabels > 0) { [self.contentView.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if ([obj isKindOfClass:[UILabel class]]) { [obj removeFromSuperview]; } }]; self.angleStep = 2 * M_PI / numberOfLabels; for (NSInteger i = 0; i < numberOfLabels; i++) { UILabel *rotatedLabel = [UILabel ayn_rotatedLabelWithText:[NSString stringWithFormat:@"%ld", i] angle:self.angleStep * i circleRadius:self.circleRadius offset:kAYNCircleViewLabelOffset font:self.labelFont textColor:self.labelTextColor]; [self.contentView addSubview:rotatedLabel]; } } } 

ダイヤルに数字を設定すると、ステップが計算されます。

 @property (assign, nonatomic) NSUInteger numberOfLabels; 

次に、パブリックプロパティを追加して、ダイヤルの桁数を設定します。

 - (void)setNumberOfLabels:(NSUInteger)numberOfLabels { _numberOfLabels = numberOfLabels; if (_isInitialized) { [self addLabelsWithNumber:_numberOfLabels]; } } 

そして、 backgroundView類推により、そのsetterを定義しbackgroundView
できた viewがすでに作成されている場合、ダイヤルの桁数を設定します。 メソッド- (void)layoutSubviews- (void)layoutSubviews初期化を忘れないでください。 署名もそこに置く必要があります。

 - (void)layoutSubviews { [super layoutSubviews]; if (!self.isInitialized) { self.isInitialized = YES; …. [self addLabelsWithNumber:self.numberOfLabels]; ... } } 

ここで- (void)viewDidLoadコントロールがviewされるviewあるコントローラーの- (void)viewDidLoadは、次の形式になります。

 - (void)viewDidLoad { [super viewDidLoad]; UIView *redView = [UIView new]; redView.backgroundColor = [UIColor redColor]; self.circleView.backgroundView = redView; self.circleView.numberOfLabels = 12; self.circleView.delegate = self; } 

viewsの階層と番号の配置を見てみましょう。



階層は真であることが判明しました-すべてのラベルはcontentViewます。

インターフェース回転サポート


一部のアプリケーションでは画面の水平方向が使用されることに注意してください。 この状況を処理するために、インターフェイスの向きの変更に関する通知( NSNotificationクラス)を追跡します。 UIDeviceOrientationDidChangeNotification興味があります。

コントロールのイニシャライザーでこの通知にobserver追加し、同じブロックで処理します。

 __weak __typeof(self) weakSelf = self; [[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceOrientationDidChangeNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { __strong __typeof(weakSelf) strongSelf = weakSelf; strongSelf.isInitialized = NO; [strongSelf setNeedsLayout]; }]; 

ブロックは暗黙的にselfキャプチャするため、これはretain cycleつながる可能性があるretain cycleselfへの参照を弱めます。 向きを変更するとき、円の半径、新しい中心などを再計算するために、コントロールをそのまま初期化します。

メソッド- (void)dealloc通知のサブスク- (void)dealloc忘れないでください。

 - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:UIDeviceOrientationDidChangeNotification object:nil]; } 

ダイヤルが実装されています。 回転の数学と、記事の第2部でカスタムコントロールを作成するための次のステップについて読んでください

プロジェクト全体がgitaで利用可能です。

Source: https://habr.com/ru/post/J326324/


All Articles