Commit f47a2561 authored by Alexandre Lision's avatar Alexandre Lision

call: add connecting animation

This commit adds an animated circular progress view when the state is in
Initialization or Connected states.
We also use LRC's HumanStateName instead of hardcoding call state in the client

Refs #75634

Change-Id: I574838d624ba9705011c463ccabc5c78857193b4
parent 8bf6ddc4
......@@ -79,6 +79,8 @@ SET(ringclient_BACKENDS
SET(ringclient_VIEWS
src/views/CallView.mm
src/views/CallView.h
src/views/ITProgressIndicator.mm
src/views/ITProgressIndicator.h
src/views/PersonCell.mm
src/views/PersonCell.h)
......
......@@ -42,6 +42,7 @@
#import <video/renderer.h>
#import <media/text.h>
#import "views/ITProgressIndicator.h"
#import "views/CallView.h"
@interface RendererConnectionsHolder : NSObject
......@@ -67,6 +68,7 @@
@property (unsafe_unretained) IBOutlet NSButton *muteAudioButton;
@property (unsafe_unretained) IBOutlet NSButton *muteVideoButton;
@property (unsafe_unretained) IBOutlet ITProgressIndicator *loadingIndicator;
@property (unsafe_unretained) IBOutlet NSTextField *timeSpentLabel;
@property (unsafe_unretained) IBOutlet NSView *controlsPanel;
......@@ -91,7 +93,7 @@
@synthesize personLabel, actionHash, stateLabel, holdOnOffButton, hangUpButton,
recordOnOffButton, pickUpButton, chatButton, timeSpentLabel,
muteVideoButton, muteAudioButton, controlsPanel, videoView,
videoLayer, previewLayer, previewView, splitView;
videoLayer, previewLayer, previewView, splitView, loadingIndicator;
@synthesize previewHolder;
@synthesize videoHolder;
......@@ -130,53 +132,42 @@
[timeSpentLabel setStringValue:callIdx.data((int)Call::Role::Length).toString().toNSString()];
Call::State state = callIdx.data((int)Call::Role::State).value<Call::State>();
[loadingIndicator setHidden:YES];
[stateLabel setStringValue:callIdx.data((int)Call::Role::HumanStateName).toString().toNSString()];
switch (state) {
case Call::State::DIALING:
[stateLabel setStringValue:@"Dialing"];
[loadingIndicator setHidden:NO];
break;
case Call::State::NEW:
[stateLabel setStringValue:@"New"];
break;
case Call::State::INITIALIZATION:
[stateLabel setStringValue:@"Initializing"];
[videoView setShouldAcceptInteractions:NO];
[loadingIndicator setHidden:NO];
break;
case Call::State::CONNECTED:
[videoView setShouldAcceptInteractions:NO];
[loadingIndicator setHidden:NO];
break;
case Call::State::RINGING:
[stateLabel setStringValue:@"Ringing"];
[videoView setShouldAcceptInteractions:NO];
break;
case Call::State::CURRENT:
[stateLabel setStringValue:@"Current"];
[videoView setShouldAcceptInteractions:YES];
break;
case Call::State::HOLD:
[stateLabel setStringValue:@"On Hold"];
[videoView setShouldAcceptInteractions:NO];
break;
case Call::State::BUSY:
[stateLabel setStringValue:@"Busy"];
[videoView setShouldAcceptInteractions:NO];
break;
case Call::State::OVER:
[stateLabel setStringValue:@"Finished"];
[videoView setShouldAcceptInteractions:NO];
if(videoView.isInFullScreenMode)
[videoView exitFullScreenModeWithOptions:nil];
break;
case Call::State::ABORTED:
[stateLabel setStringValue:@"Aborted"];
break;
case Call::State::FAILURE:
[stateLabel setStringValue:@"Failure"];
[videoView setShouldAcceptInteractions:NO];
break;
case Call::State::INCOMING:
[stateLabel setStringValue:@"Incoming"];
break;
default:
[stateLabel setStringValue:@""];
break;
}
}
......@@ -221,6 +212,12 @@
previewHolder = [[RendererConnectionsHolder alloc] init];
videoHolder = [[RendererConnectionsHolder alloc] init];
[loadingIndicator setColor:[NSColor whiteColor]];
[loadingIndicator setNumberOfLines:100];
[loadingIndicator setWidthOfLine:2];
[loadingIndicator setLengthOfLine:2];
[loadingIndicator setInnerMargin:30];
[self.videoView setFullScreenDelegate:self];
[self connect];
......
//Copyright 2013-2015 Ilija Tovilo
//
//Licensed under the Apache License, Version 2.0 (the "License");
//you may not use this file except in compliance with the License.
//You may obtain a copy of the License at
//
//http://www.apache.org/licenses/LICENSE-2.0
//
//Unless required by applicable law or agreed to in writing, software
//distributed under the License is distributed on an "AS IS" BASIS,
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//See the License for the specific language governing permissions and
//limitations under the License.
#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>
//
// !!!IMPORTANT!!! - Embedd ITProgressIndicator in a layer-backed view to avoid side-effects!
//
/**
* @class ITProgressIndicator
*
* A replacement for `NSProgressIndicator`.
* It's a highly customizable control, driven by Core Animation, which makes it much more performant.
*
* So basically, it's awesome.
*
*/
@interface ITProgressIndicator : NSView
#pragma mark - Methods
/**
* Override this method to achieve a custom animation
*
* @return CAKeyframeAnimation - animation which will be put on the progress indicator layer
*/
- (CAKeyframeAnimation *)keyFrameAnimationForCurrentPreferences;
#pragma mark - Properties
/// @property isIndeterminate - Indicates if the view will show the progress, or just spin
@property (nonatomic, setter = setIndeterminate:) BOOL isIndeterminate;
/// @property progress - The amount that should be shown when `isIndeterminate` is set to `YES`
@property (nonatomic) CGFloat progress;
/// @property animates - Indicates if the view is animating
@property (nonatomic) BOOL animates;
/// @property hideWhenStopped - Indicates if the view will be hidden if it's stopped
@property (nonatomic) BOOL hideWhenStopped;
/// @property lengthOfLine - The length of a single line
@property (nonatomic) CGFloat lengthOfLine;
/// @property widthOfLine - The width of a single line
@property (nonatomic) CGFloat widthOfLine;
/// @property numberOfLines - The number of lines of the indicator
@property (nonatomic) NSUInteger numberOfLines;
/// @property innerMargin - The distance of the lines from the middle
@property (nonatomic) CGFloat innerMargin;
/// @property animationDuration - Duration of a single rotation
@property (nonatomic) CGFloat animationDuration;
/// @property gradualAnimation - Defines if the animation is smooth or gradual
@property (nonatomic) BOOL steppedAnimation;
/// @property color - The color of the progress indicator
@property (nonatomic, strong) NSColor *color;
@end
//Copyright 2013-2015 Ilija Tovilo
//
//Licensed under the Apache License, Version 2.0 (the "License");
//you may not use this file except in compliance with the License.
//You may obtain a copy of the License at
//
//http://www.apache.org/licenses/LICENSE-2.0
//
//Unless required by applicable law or agreed to in writing, software
//distributed under the License is distributed on an "AS IS" BASIS,
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//See the License for the specific language governing permissions and
//limitations under the License.
#if !__has_feature(objc_arc)
#error ARC needs to be enabled!
#endif
#import "ITProgressIndicator.h"
#pragma mark - Consts
#define kITSpinAnimationKey @"spinAnimation"
#define kITProgressPropertyKey @"progress"
// ----------------------------------------------------------------------------------------
#pragma mark - NSBezierPath+IT_Geometry
// ----------------------------------------------------------------------------------------
@interface NSBezierPath (IT_Geometry)
- (NSBezierPath*)it_rotatedBezierPath:(float) angle;
- (NSBezierPath*)it_rotatedBezierPath:(float) angle aboutPoint:(NSPoint)point;
@end
@implementation NSBezierPath (IT_Geometry)
- (NSBezierPath *)it_rotatedBezierPath:(float)angle {
return [self it_rotatedBezierPath:angle aboutPoint:NSMakePoint(NSMidX(self.bounds), NSMidY(self.bounds))];
}
- (NSBezierPath*)it_rotatedBezierPath:(float)angle aboutPoint:(NSPoint)point {
if(angle == 0.0) return self;
else
{
NSBezierPath* copy = [self copy];
NSAffineTransform *xfm = [self it_rotationTransformWithAngle:angle aboutPoint:point];
[copy transformUsingAffineTransform:xfm];
return copy;
}
}
- (NSAffineTransform *)it_rotationTransformWithAngle:(const float)angle aboutPoint:(const NSPoint)aboutPoint {
NSAffineTransform *xfm = [NSAffineTransform transform];
[xfm translateXBy:aboutPoint.x yBy:aboutPoint.y];
[xfm rotateByRadians:angle];
[xfm translateXBy:-aboutPoint.x yBy:-aboutPoint.y];
return xfm;
}
@end
// ----------------------------------------------------------------------------------------
#pragma mark - ITProgressIndicator
// ----------------------------------------------------------------------------------------
#pragma mark - Private Interface
@interface ITProgressIndicator ()
@property (nonatomic, strong, readonly) CALayer *rootLayer;
@property (nonatomic, strong, readonly) CALayer *progressIndicatorLayer;
@end
#pragma mark - Implementation
@implementation ITProgressIndicator
@synthesize progressIndicatorLayer = _progressIndicatorLayer;
#pragma mark - Init
- (id)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self initLayers];
}
return self;
}
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self initLayers];
}
return self;
}
- (void)initLayers {
// Setting initial values
self.color = [NSColor blackColor];
self.innerMargin = 4;
self.widthOfLine = 3;
self.lengthOfLine = 6;
self.numberOfLines = 8;
self.animationDuration = 0.6;
self.isIndeterminate = YES;
self.steppedAnimation = YES;
self.hideWhenStopped = YES;
self.animates = YES;
// Init layers
_rootLayer = [CALayer layer];
self.layer = _rootLayer;
[self setWantsLayer:YES];
self.progressIndicatorLayer.frame = _rootLayer.bounds;
[_rootLayer addSublayer:self.progressIndicatorLayer];
[self reloadIndicatorContent];
[self reloadAnimation];
}
- (void)awakeFromNib {
[self reloadAnimation];
}
- (void)reloadIndicatorContent {
self.progressIndicatorLayer.contents = [self progressImage];
}
- (void)reloadAnimation {
[self.progressIndicatorLayer removeAnimationForKey:kITSpinAnimationKey];
if (self.animates) {
[self.progressIndicatorLayer addAnimation:[self keyFrameAnimationForCurrentPreferences] forKey:kITSpinAnimationKey];
}
}
#pragma mark - Drawing
- (NSImage *)progressImage {
NSImage *progressImage = [[NSImage alloc] initWithSize:self.bounds.size];
[progressImage lockFocus];
{
[NSGraphicsContext saveGraphicsState];
{
[self.color set];
NSRect r = self.bounds;
NSBezierPath *line = [NSBezierPath bezierPathWithRoundedRect:
NSMakeRect((NSWidth(r) / 2) - (self.widthOfLine / 2),
(NSHeight(r) / 2) - self.innerMargin - self.lengthOfLine,
self.widthOfLine, self.lengthOfLine)
xRadius:self.widthOfLine / 2
yRadius:self.widthOfLine / 2];
void (^lineDrawingBlock)(NSUInteger line) =
^(NSUInteger lineNumber) {
NSBezierPath *lineInstance = [line copy];
lineInstance = [lineInstance it_rotatedBezierPath:((2 * M_PI) / self.numberOfLines * lineNumber) + M_PI
aboutPoint:NSMakePoint(NSWidth(r) / 2, NSHeight(r) / 2)];
if (_isIndeterminate) [[self.color colorWithAlphaComponent:1.0 - (1.0 / self.numberOfLines * lineNumber)] set];
[lineInstance fill];
};
if (!self.isIndeterminate) {
for (NSUInteger i = self.numberOfLines;
i > round(self.numberOfLines - (self.numberOfLines * self.progress));
i--)
{
lineDrawingBlock(i);
}
} else {
for (NSUInteger i = 0; i < self.numberOfLines; i++) {
lineDrawingBlock(i);
}
}
}
[NSGraphicsContext restoreGraphicsState];
}
[progressImage unlockFocus];
return progressImage;
}
#pragma mark - Helpers
- (CAKeyframeAnimation *)keyFrameAnimationForCurrentPreferences {
NSMutableArray* keyFrameValues = [NSMutableArray array];
NSMutableArray* keyTimeValues;
if (self.steppedAnimation) {
{
[keyFrameValues addObject:[NSNumber numberWithFloat:0.0]];
for (NSUInteger i = 0; i < self.numberOfLines; i++) {
[keyFrameValues addObject:[NSNumber numberWithFloat:-M_PI * (2.0 / self.numberOfLines * i)]];
[keyFrameValues addObject:[NSNumber numberWithFloat:-M_PI * (2.0 / self.numberOfLines * i)]];
}
[keyFrameValues addObject:[NSNumber numberWithFloat:-M_PI*2.0]];
}
keyTimeValues = [NSMutableArray array];
{
[keyTimeValues addObject:[NSNumber numberWithFloat:0.0]];
for (NSUInteger i = 0; i < (self.numberOfLines - 1); i++) {
[keyTimeValues addObject:[NSNumber numberWithFloat:1.0 / self.numberOfLines * i]];
[keyTimeValues addObject:[NSNumber numberWithFloat:1.0 / self.numberOfLines * (i + 1)]];
}
[keyTimeValues addObject:[NSNumber numberWithFloat:1.0 / self.numberOfLines * (self.numberOfLines - 1)]];
}
} else {
{
[keyFrameValues addObject:[NSNumber numberWithFloat:-M_PI*0.0]];
[keyFrameValues addObject:[NSNumber numberWithFloat:-M_PI*0.5]];
[keyFrameValues addObject:[NSNumber numberWithFloat:-M_PI*1.0]];
[keyFrameValues addObject:[NSNumber numberWithFloat:-M_PI*1.5]];
[keyFrameValues addObject:[NSNumber numberWithFloat:-M_PI*2.0]];
}
}
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
[animation setRepeatCount:HUGE_VALF];
[animation setValues:keyFrameValues];
[animation setKeyTimes:keyTimeValues];
[animation setValueFunction:[CAValueFunction functionWithName: kCAValueFunctionRotateZ]];
[animation setDuration:self.animationDuration];
return animation;
}
- (void)reloadVisibility {
if (_hideWhenStopped && !_animates && _isIndeterminate) {
[self setHidden:YES];
} else {
[self setHidden:NO];
}
}
#pragma mark - NSView methods
// Animatible proxy
+ (id)defaultAnimationForKey:(NSString *)key
{
if ([key isEqualToString:kITProgressPropertyKey]) {
return [CABasicAnimation animation];
} else {
return [super defaultAnimationForKey:key];
}
}
#pragma mark - Setters & Getters
- (void)setIndeterminate:(BOOL)isIndeterminate {
_isIndeterminate = isIndeterminate;
if (!_isIndeterminate) {
self.animates = NO;
}
}
- (void)setProgress:(CGFloat)progress {
if (progress < 0 || progress > 1) {
@throw [NSException exceptionWithName:@"Invalid `progress` property value"
reason:@"`progress` property needs to be between 0 and 1"
userInfo:nil];
}
_progress = progress;
if (!self.isIndeterminate) {
[self reloadIndicatorContent];
}
}
- (void)setAnimates:(BOOL)animates {
_animates = animates;
[self reloadIndicatorContent];
[self reloadAnimation];
[self reloadVisibility];
}
- (void)setHideWhenStopped:(BOOL)hideWhenStopped {
_hideWhenStopped = hideWhenStopped;
[self reloadVisibility];
}
- (CALayer *)progressIndicatorLayer {
if (!_progressIndicatorLayer) {
_progressIndicatorLayer = [CALayer layer];
}
return _progressIndicatorLayer;
}
- (void)setLengthOfLine:(CGFloat)lengthOfLine {
_lengthOfLine = lengthOfLine;
[self reloadIndicatorContent];
}
- (void)setWidthOfLine:(CGFloat)widthOfLine {
_widthOfLine = widthOfLine;
[self reloadIndicatorContent];
}
- (void)setInnerMargin:(CGFloat)innerMargin {
_innerMargin = innerMargin;
[self reloadIndicatorContent];
}
- (void)setAnimationDuration:(CGFloat)animationDuration {
_animationDuration = animationDuration;
[self reloadAnimation];
}
- (void)setNumberOfLines:(NSUInteger)numberOfLines {
_numberOfLines = numberOfLines;
[self reloadIndicatorContent];
[self reloadAnimation];
}
- (void)setSteppedAnimation:(BOOL)steppedAnimation {
_steppedAnimation = steppedAnimation;
[self reloadAnimation];
}
- (void)setColor:(NSColor *)color {
_color = color;
[self reloadIndicatorContent];
}
@end
......@@ -10,6 +10,7 @@
<outlet property="controlsPanel" destination="Eoi-B8-iL6" id="4xn-3b-SNn"/>
<outlet property="hangUpButton" destination="Kjq-iM-NBL" id="Puz-4L-Okl"/>
<outlet property="holdOnOffButton" destination="anb-Y8-JQi" id="HSl-pE-Kwg"/>
<outlet property="loadingIndicator" destination="JwW-2h-DyZ" id="EEb-50-oSJ"/>
<outlet property="muteAudioButton" destination="tQl-cT-0Lb" id="qV4-Ef-UTx"/>
<outlet property="muteVideoButton" destination="LVS-yZ-98V" id="qQs-zP-wQ4"/>
<outlet property="personLabel" destination="bg3-hB-nE8" id="t6l-1B-JxI"/>
......@@ -39,19 +40,8 @@
<customView translatesAutoresizingMaskIntoConstraints="NO" id="d0X-cW-Xgz">
<rect key="frame" x="20" y="438" width="635" height="71"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="bg3-hB-nE8">
<rect key="frame" x="18" y="40" width="85" height="17"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="81" id="gT7-Wu-XtU"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="left" title="Person name" id="osk-LS-0Qg">
<font key="font" metaFont="system"/>
<color key="textColor" name="highlightColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="kFD-FB-vig">
<rect key="frame" x="18" y="20" width="37" height="17"/>
<rect key="frame" x="18" y="42" width="37" height="17"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="29" id="pft-oc-ZNh"/>
</constraints>
......@@ -75,16 +65,27 @@
<userDefinedRuntimeAttribute type="string" keyPath="layer.cornerRadius" value="15"/>
</userDefinedRuntimeAttributes>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="bg3-hB-nE8">
<rect key="frame" x="18" y="17" width="85" height="17"/>
<constraints>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="81" id="gT7-Wu-XtU"/>
</constraints>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="left" title="Person name" id="osk-LS-0Qg">
<font key="font" metaFont="system"/>
<color key="textColor" name="highlightColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="kFD-FB-vig" firstAttribute="leading" secondItem="bg3-hB-nE8" secondAttribute="leading" id="LXG-QI-oPf"/>
<constraint firstItem="cIU-M7-xpN" firstAttribute="top" secondItem="d0X-cW-Xgz" secondAttribute="top" constant="24" id="Qc7-qp-qSV"/>
<constraint firstAttribute="trailing" secondItem="cIU-M7-xpN" secondAttribute="trailing" constant="20" id="RXf-xZ-4f9"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="568" id="Xeq-Aa-f1W"/>
<constraint firstItem="kFD-FB-vig" firstAttribute="top" secondItem="bg3-hB-nE8" secondAttribute="bottom" constant="3" id="Z06-5v-81Q"/>
<constraint firstItem="kFD-FB-vig" firstAttribute="top" secondItem="bg3-hB-nE8" secondAttribute="bottom" constant="3" id="gRn-E6-o6O"/>
<constraint firstItem="kFD-FB-vig" firstAttribute="top" secondItem="bg3-hB-nE8" secondAttribute="bottom" constant="-42" id="Z06-5v-81Q"/>
<constraint firstItem="kFD-FB-vig" firstAttribute="top" secondItem="bg3-hB-nE8" secondAttribute="bottom" constant="-42" id="gRn-E6-o6O"/>
<constraint firstItem="kFD-FB-vig" firstAttribute="leading" secondItem="d0X-cW-Xgz" secondAttribute="leading" constant="20" id="i5C-8o-qKp"/>
<constraint firstAttribute="bottom" secondItem="kFD-FB-vig" secondAttribute="bottom" constant="20" id="l71-7V-oLx"/>
<constraint firstAttribute="bottom" secondItem="kFD-FB-vig" secondAttribute="bottom" constant="42" id="l71-7V-oLx"/>
<constraint firstItem="bg3-hB-nE8" firstAttribute="leading" secondItem="d0X-cW-Xgz" secondAttribute="leading" constant="20" id="nV4-Vy-vqK"/>
<constraint firstAttribute="centerY" secondItem="cIU-M7-xpN" secondAttribute="centerY" id="yvc-8B-cEu"/>
</constraints>
......@@ -233,13 +234,22 @@
<constraint firstItem="qgD-3D-nD5" firstAttribute="leading" secondItem="oRa-pS-HN2" secondAttribute="trailing" constant="-89" id="wQF-FD-dbj"/>
</constraints>
</customView>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="JwW-2h-DyZ" customClass="ITProgressIndicator">
<rect key="frame" x="287" y="205" width="100" height="100"/>
<constraints>
<constraint firstAttribute="height" constant="100" id="gpl-re-hHE"/>
<constraint firstAttribute="width" constant="100" id="nmo-HF-lhL"/>
</constraints>
</customView>
</subviews>
<constraints>
<constraint firstAttribute="centerX" secondItem="JwW-2h-DyZ" secondAttribute="centerX" id="4eh-az-oI5"/>
<constraint firstItem="6y6-RH-qOp" firstAttribute="leading" secondItem="Eoi-B8-iL6" secondAttribute="trailing" constant="8" id="7wV-uh-Xb7"/>
<constraint firstAttribute="trailing" secondItem="d0X-cW-Xgz" secondAttribute="trailing" constant="20" id="G79-Jv-EYw"/>
<constraint firstAttribute="bottom" secondItem="6y6-RH-qOp" secondAttribute="bottom" constant="20" id="HOt-7O-FU2"/>
<constraint firstAttribute="trailing" secondItem="6y6-RH-qOp" secondAttribute="trailing" constant="20" id="KTx-SN-RUg"/>
<constraint firstItem="d0X-cW-Xgz" firstAttribute="top" secondItem="2wf-Py-l6B" secondAttribute="top" id="MKB-zm-C75"/>
<constraint firstAttribute="centerY" secondItem="JwW-2h-DyZ" secondAttribute="centerY" id="Na1-o4-4Ds"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="675" id="ciq-ed-2FK"/>
<constraint firstItem="d0X-cW-Xgz" firstAttribute="leading" secondItem="2wf-Py-l6B" secondAttribute="leading" constant="20" id="efy-70-qsJ"/>
<constraint firstAttribute="bottom" secondItem="Eoi-B8-iL6" secondAttribute="bottom" constant="20" id="glQ-Is-Pk6"/>
......