11月17日、モスクワで、モバイル開発者
MBLTdevに関する国際会議の一環として
、 Alexander Ziminが「UIKitの標準コンポーネントを超えた視覚化」に関するプレゼンテーションを行いました。 まず、このレポートは、カスタムUI要素の開発について詳しく知りたいiOS開発者を対象としています。 彼は、カスタムコントロールの例に興味があり、レポートに記載されている点を考慮して実装し、改良することにしました。 この例は
Swiftで実装されましたが、 
Objective-C実装しています。
カスタムUI要素を開発する方法:
- 基本要素がどのように機能するかを理解する必要がありdataSource。そのすべてのプロパティ、メソッド、delegate、およびdataSourceを調べるためです。
- UIView+依存要素を設計します。- UIViewを表示するユニバーサルソリューションを作成する必要があります。 たとえば、要素には- contentViewます。 ユーザーがUI要素の実装を考慮することなく、- UIViewを割り当てることができるように設計する必要があります。
- UIControl忘れないでください。 カスタムボタンまたは他のコントロールが必要な場合は、- UIViewからでは- UIControlから継承することをお- UIControlます。- UIControlには- Target-Actionシステムがあり、ボタンから- Interface Builderからコードに直接- IBActionを「ストレッチ」できます。- UIViewに対する利点は、状態の存在とより良いタッチトラッキングです。
- あなたはあなたのコンポーネントに近いコンポーネントを研究する必要があります。
- さまざまなデバイスの機能、特にアクションコンポーネントを操作する際のiPhone 7の触覚振動(クラスUIImpactFeedbackGenerator)を忘れないでください。
実装されるもの
このレポートは、 
UIPickerView似たカスタム
UIView例でした。 タイミングを目的としています。

このコンポーネントは
UIPickerView似てい
UIPickerView 。 したがって、以下を実装する必要があります。
- 自動スピン;
- ドラムは要素で停止します。
 
- iPhone 7にはフィードバック振動が必要です(私は実装していません)。
実装方法
UIView取り、それを丸くして、その
UILabelに数字のある
UILabelを
UILabelします。 回転するには、無限の
contentSizeを持つ
UIScrollViewを追加し、シフトに基づいて回転角度を考慮します。

必要です:
- UIScrollViewでシフト- x、- yを計算し、
- 方向を認識する
- contentViewひねり、
- 目的のアイテムにネジ止めする
- UIViewを置き換える機会を与えます。
階層の準備
AYNCircleView作成します。 これは、カスタム要素全体を含むクラスになります。 この段階では、彼には何も公開されていません。私たちはすべて個人的に行っています。 次に、階層の作成を開始します。 まず、 
Interface Builder viewを作成します。 
AYNCircleView.xibを作成して、階層を処理しましょう。

階層は次の要素で構成されます。
- contentView他のすべての- subviewsが- subviewsされる円、
- scrollViewは回転を提供します。
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で利用可能です。