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

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



で実装されましたが、 Objective-C



実装しています。



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





実装されるもの



このレポートは、 UIPickerView



似たカスタムUIView



例でした。 タイミングを目的としています。







このコンポーネントはUIPickerView



似ていUIPickerView



。 したがって、以下を実装する必要があります。





実装方法



UIView



取り、それを丸くして、そのUILabel



に数字のあるUILabel



UILabel



します。 回転するには、無限のcontentSize



を持つUIScrollView



を追加し、シフトに基づいて回転角度を考慮します。







必要です:





階層の準備



AYNCircleView



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



view



を作成します。 AYNCircleView.xib



を作成して、階層を処理しましょう。







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





constraints



設定しましょう。 私たちが最も興味を持っているのはcontentView



bottom space



高さです。 サークルのサイズと位置を提供します。 残りのconstraints



は、 contentView



superview



を超えてsuperview



を防ぎます。 便宜上、 scrollView



contentSize



側を定数でscrollView



ます。 これはパフォーマンスに大きな影響を与えませんが、回転の「無限」をシミュレートします。 ささいなことに気をscrollView



ば、「ジャンプ」システムを実装してscrollView



contentSize



を大幅に減らすことが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



を作成し、 xOffset



yOffset



使用して、この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 cycle



self



への参照を弱めます。 向きを変更するとき、円の半径、新しい中心などを再計算するために、コントロールをそのまま初期化します。



メソッド- (void)dealloc



通知のサブスク- (void)dealloc



忘れないでください。



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





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



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



All Articles