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で利用可能です。