Commit fd9f3ab1 authored by Silbino Goncalves Matado's avatar Silbino Goncalves Matado Committed by Silbino Goncalves Matado

CreateRingAccount: Add Rx+MVVM bindings for the form validation

Add some bindings from the CreateRingAccountViewController to the
CreateRingAccountViewModel to validate the following user inputs :

- username (optional)
- password (must be 6 characters min.)
- repeat password (must be equal to the password)

If conditions are not verified. The Create Account button is
disabled.

Bindings are made with Variabes and Observers into the ViewModel.

Tuleap: #1400
Change-Id: Ib03797e365972aefafbc75c5a8b798e8ba659cfc
parent eec4d581
......@@ -101,8 +101,6 @@
04399B141D1C341A00E99CD9 /* libx264.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 04399AE21D1C341A00E99CD9 /* libx264.a */; };
04399B151D1C341A00E99CD9 /* libyaml-cpp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 04399AE31D1C341A00E99CD9 /* libyaml-cpp.a */; };
5516C29F1E71CEFF009D3D2D /* AccountModelHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5516C29E1E71CEFF009D3D2D /* AccountModelHelper.swift */; };
564C44591E8D7F8F000F92B1 /* LocalizedStringTableNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C44581E8D7F8F000F92B1 /* LocalizedStringTableNames.swift */; };
56AC64D51E7C7F4000EA1AA9 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64D41E7C7F4000EA1AA9 /* WelcomeViewController.swift */; };
5557FD4A1E81AE850043E394 /* AccountModelHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5557FD491E81AE850043E394 /* AccountModelHelperTests.swift */; };
5557FD4B1E81AECF0043E394 /* AccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02B22DFA1DF755BB000358C9 /* AccountModel.swift */; };
5557FD4C1E81AF840043E394 /* AccountConfigModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DD80C71E1EAD70009A3510 /* AccountConfigModel.swift */; };
......@@ -110,11 +108,15 @@
5557FD4E1E81B1F20043E394 /* AccountModelHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5516C29E1E71CEFF009D3D2D /* AccountModelHelper.swift */; };
5557FD4F1E81B2990043E394 /* AccountCredentialsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DD80C91E1EAF1A009A3510 /* AccountCredentialsModel.swift */; };
557086521E8ADB9D001A7CE4 /* SystemAdapter.mm in Sources */ = {isa = PBXBuildFile; fileRef = 557086511E8ADB9D001A7CE4 /* SystemAdapter.mm */; };
564C44591E8D7F8F000F92B1 /* LocalizedStringTableNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C44581E8D7F8F000F92B1 /* LocalizedStringTableNames.swift */; };
564C445B1E8EA44E000F92B1 /* Durations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564C445A1E8EA44E000F92B1 /* Durations.swift */; };
56AC64D51E7C7F4000EA1AA9 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64D41E7C7F4000EA1AA9 /* WelcomeViewController.swift */; };
56AC64D91E8012CA00EA1AA9 /* Walkthrough.strings in Resources */ = {isa = PBXBuildFile; fileRef = 56AC64DB1E8012CA00EA1AA9 /* Walkthrough.strings */; };
56AC64DF1E804ECC00EA1AA9 /* SwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64DE1E804ECC00EA1AA9 /* SwitchCell.swift */; };
56AC64E11E80542300EA1AA9 /* TextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64E01E80542300EA1AA9 /* TextFieldCell.swift */; };
56AC64E31E805F0200EA1AA9 /* TextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC64E21E805F0200EA1AA9 /* TextCell.swift */; };
56AC650E1E85694D00EA1AA9 /* RoundedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56AC650D1E85694D00EA1AA9 /* RoundedTextField.swift */; };
56BBC9C51ED8BF3300CDAF8B /* libargon2.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 56BBC9C41ED8BF3300CDAF8B /* libargon2.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
......@@ -243,16 +245,18 @@
04399AE21D1C341A00E99CD9 /* libx264.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libx264.a; path = ../fat/lib/libx264.a; sourceTree = "<group>"; };
04399AE31D1C341A00E99CD9 /* libyaml-cpp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libyaml-cpp.a"; path = "../fat/lib/libyaml-cpp.a"; sourceTree = "<group>"; };
5516C29E1E71CEFF009D3D2D /* AccountModelHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AccountModelHelper.swift; path = Account/AccountModelHelper.swift; sourceTree = "<group>"; };
564C44581E8D7F8F000F92B1 /* LocalizedStringTableNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizedStringTableNames.swift; sourceTree = "<group>"; };
56AC64D41E7C7F4000EA1AA9 /* WelcomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
5557FD491E81AE850043E394 /* AccountModelHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountModelHelperTests.swift; sourceTree = "<group>"; };
557086501E8ADB9D001A7CE4 /* SystemAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SystemAdapter.h; path = Bridging/SystemAdapter.h; sourceTree = "<group>"; };
557086511E8ADB9D001A7CE4 /* SystemAdapter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = SystemAdapter.mm; path = Bridging/SystemAdapter.mm; sourceTree = "<group>"; };
564C44581E8D7F8F000F92B1 /* LocalizedStringTableNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizedStringTableNames.swift; sourceTree = "<group>"; };
564C445A1E8EA44E000F92B1 /* Durations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Durations.swift; sourceTree = "<group>"; };
56AC64D41E7C7F4000EA1AA9 /* WelcomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
56AC64DA1E8012CA00EA1AA9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Walkthrough.strings; sourceTree = "<group>"; };
56AC64DE1E804ECC00EA1AA9 /* SwitchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchCell.swift; sourceTree = "<group>"; };
56AC64E01E80542300EA1AA9 /* TextFieldCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldCell.swift; sourceTree = "<group>"; };
56AC64E21E805F0200EA1AA9 /* TextCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextCell.swift; sourceTree = "<group>"; };
56AC650D1E85694D00EA1AA9 /* RoundedTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedTextField.swift; sourceTree = "<group>"; };
56BBC9C41ED8BF3300CDAF8B /* libargon2.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libargon2.a; path = ../fat/lib/libargon2.a; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
......@@ -260,6 +264,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
56BBC9C51ED8BF3300CDAF8B /* libargon2.a in Frameworks */,
02674C851E0C757B0065EDF9 /* RxCocoa.framework in Frameworks */,
02674C861E0C757B0065EDF9 /* RxSwift.framework in Frameworks */,
043866211D218B1100E06CE2 /* AudioToolbox.framework in Frameworks */,
......@@ -371,6 +376,7 @@
02AED8171DD4C4B000F740BA /* Frameworks */ = {
isa = PBXGroup;
children = (
56BBC9C41ED8BF3300CDAF8B /* libargon2.a */,
02674C801E0C757B0065EDF9 /* RxBlocking.framework */,
02674C811E0C757B0065EDF9 /* RxCocoa.framework */,
02674C821E0C757B0065EDF9 /* RxSwift.framework */,
......@@ -609,6 +615,7 @@
isa = PBXGroup;
children = (
564C44581E8D7F8F000F92B1 /* LocalizedStringTableNames.swift */,
564C445A1E8EA44E000F92B1 /* Durations.swift */,
);
name = Constants;
sourceTree = "<group>";
......@@ -808,6 +815,7 @@
04399AAE1D1C304300E99CD9 /* Utils.mm in Sources */,
02B22DFD1DF755BB000358C9 /* CreateRingAccountViewModel.swift in Sources */,
043999FA1D1C2D9D00E99CD9 /* Ring.xcdatamodeld in Sources */,
564C445B1E8EA44E000F92B1 /* Durations.swift in Sources */,
0438663B1D2313B700E06CE2 /* AccountDetailsViewController.swift in Sources */,
02DD80CA1E1EAF1A009A3510 /* AccountCredentialsModel.swift in Sources */,
0273C3081E0C68BF00CF00BA /* RoundedButton.swift in Sources */,
......
......@@ -43,13 +43,79 @@ class CreateRingAccountViewModel {
*/
fileprivate var account: AccountModel?
var registerUsername = Variable<Bool>(true)
/**
The accountService instance injected in initializer.
*/
fileprivate let accountService: AccountsService
//MARK: - Rx Variables and Observers
var username = Variable<String>("")
var password = Variable<String>("")
var repeatPassword = Variable<String>("")
var usernameValid :Observable<Bool> {
return username.asObservable().map({ username in
return !username.isEmpty
})
}
var passwordValid :Observable<Bool> {
return Observable<Bool>.combineLatest(self.username.asObservable(),
self.password.asObservable(),
self.repeatPassword.asObservable())
{ (username, password, repeatPassword) in
return password.characters.count >= 6
}
}
var passwordsEqual :Observable<Bool> {
return Observable<Bool>.combineLatest(self.password.asObservable(),
self.repeatPassword.asObservable())
{ password, repeatPassword in
return password == repeatPassword
}
}
var canCreateAccount :Observable<Bool> {
return Observable<Bool>.combineLatest(self.registerUsername.asObservable(),
self.usernameValid,
self.passwordValid,
self.passwordsEqual)
{ registerUsername, usernameValid, passwordValid, passwordsEquals in
if registerUsername {
return usernameValid && passwordValid && passwordsEquals
} else {
return passwordValid && passwordsEquals
}
}
}
var registerUsername = Variable<Bool>(true)
//Observes if the field is not empty
var hasNewPassword :Observable<Bool> {
return self.password.asObservable().map({ password in
return password.characters.count == 0
})
}
//Observes if the password is valid and is not empty to show the error message
var hidePasswordError :Observable<Bool> {
return Observable<Bool>.combineLatest(self.passwordValid, hasNewPassword)
{ isPasswordValid, hasNewPassword in
return isPasswordValid || hasNewPassword
}
}
//Observes if the password is valid and is not empty to show the error message
var hideRepeatPasswordError :Observable<Bool> {
return Observable<Bool>.combineLatest(self.passwordValid, self.passwordsEqual) { isPasswordValid,
isPasswordsEquals in
return !isPasswordValid || isPasswordsEquals
}
}
/**
Default constructor
*/
......@@ -119,4 +185,5 @@ class CreateRingAccountViewModel {
})
.addDisposableTo(disposeBag)
}
}
......@@ -33,3 +33,6 @@
"EnterNewUsernamePlaceholder" = "Enter new username";
"NewPasswordPlaceholder" = "New Password";
"RepeatPasswordPlaceholder" = "Repeat new password";
"PasswordCharactersNumberError" = "6 characters minimum";
"PasswordNotMatchingError" = "Passwords do not match";
"LookingForUsernameAvailability" = "Looking for username availability...";
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import Foundation
/**
Time interval between TextField events in seconds
*/
let textFieldThrottlingDuration = 0.5
......@@ -23,6 +23,7 @@ import UIKit
class TextFieldCell: UITableViewCell {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var errorMessageLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
......
......@@ -88,6 +88,9 @@ class CreateRingAccountViewController: UITableViewController {
.subscribe(onNext: { [weak self] showUsernameField in
self?.toggleRegisterSwitch(showUsernameField)
}).addDisposableTo(mDisposeBag)
_ = self.mAccountViewModel.canCreateAccount
.bindTo(self.mCreateAccountButton.rx.isEnabled).addDisposableTo(mDisposeBag)
}
/**
......@@ -171,6 +174,17 @@ class CreateRingAccountViewController: UITableViewController {
cell.textField.placeholder = NSLocalizedString("EnterNewUsernamePlaceholder",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
//Binds the username field value to the ViewModel
_ = cell.textField.rx.text.orEmpty
.bindTo(self.mAccountViewModel.username)
.addDisposableTo(mDisposeBag)
//Switch to new password cell when return button is touched
_ = cell.textField.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: {
self.switchToCell(withType: .newPasswordField)
}).addDisposableTo(mDisposeBag)
return cell
} else if currentCellType == .passwordNotice {
let cell = tableView.dequeueReusableCell(withIdentifier: mTableViewCellId,
......@@ -186,6 +200,25 @@ class CreateRingAccountViewController: UITableViewController {
cell.textField.placeholder = NSLocalizedString("NewPasswordPlaceholder",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
cell.errorMessageLabel.text = NSLocalizedString("PasswordCharactersNumberError",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
//Binds the password field value to the ViewModel
_ = cell.textField.rx.text.orEmpty.bindTo(self.mAccountViewModel.password)
.addDisposableTo(mDisposeBag)
//Binds the observer to show the error label if the field is not empty
_ = self.mAccountViewModel.hidePasswordError.bindTo(cell.errorMessageLabel.rx.isHidden)
.addDisposableTo(mDisposeBag)
//Switch to the repeat pasword cell when return button is touched
_ = cell.textField.rx.controlEvent(.editingDidEndOnExit)
.subscribe(onNext: {
self.switchToCell(withType: .repeatPasswordField)
}).addDisposableTo(mDisposeBag)
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: mTextFieldCellId,
......@@ -194,8 +227,31 @@ class CreateRingAccountViewController: UITableViewController {
cell.textField.placeholder = NSLocalizedString("RepeatPasswordPlaceholder",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
cell.errorMessageLabel.text = NSLocalizedString("PasswordNotMatchingError",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
//Binds the repeat password field value to the ViewModel
_ = cell.textField.rx.text.orEmpty.bindTo(self.mAccountViewModel.repeatPassword)
.addDisposableTo(mDisposeBag)
//Binds the observer to the text field 'hidden' property
_ = self.mAccountViewModel.hideRepeatPasswordError.bindTo(cell.errorMessageLabel.rx.isHidden)
.addDisposableTo(mDisposeBag)
return cell
}
}
fileprivate func switchToCell(withType cellType: CreateRingAccountCellType) {
if let cellIndex = self.mCells.index(of: cellType) {
if let cell = tableView.cellForRow(at: IndexPath(row: cellIndex, section: 0))
as? TextFieldCell {
cell.textField.becomeFirstResponder()
}
self.tableView.scrollToRow(at: IndexPath(row: cellIndex, section: 0),
at: .bottom, animated: false)
}
}
}
......@@ -164,7 +164,7 @@
<scene sceneID="XTt-go-ZNJ">
<objects>
<tableViewController id="kzh-87-Ao9" customClass="CreateRingAccountViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="69" sectionHeaderHeight="28" sectionFooterHeight="28" id="e9y-lu-Idq">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" allowsSelection="NO" rowHeight="69" sectionHeaderHeight="28" sectionFooterHeight="28" id="e9y-lu-Idq">
<rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.039215686270000001" green="0.4549019608" blue="0.53725490200000003" alpha="1" colorSpace="calibratedRGB"/>
......@@ -242,34 +242,47 @@
<outlet property="titleLabel" destination="l6E-2a-51c" id="28k-pL-Ri6"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="TextFieldCellId" rowHeight="69" id="hll-q9-QzR" customClass="TextFieldCell" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="0.0" y="227" width="320" height="69"/>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="TextFieldCellId" rowHeight="141" id="hll-q9-QzR" customClass="TextFieldCell" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="0.0" y="227" width="320" height="141"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="hll-q9-QzR" id="b0d-cJ-IJT">
<rect key="frame" x="0.0" y="0.0" width="320" height="69"/>
<rect key="frame" x="0.0" y="0.0" width="320" height="141"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="dWn-2T-XDN" customClass="RoundedTextField" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="16" y="8" width="288" height="52.5"/>
<rect key="frame" x="16" y="8" width="288" height="104"/>
<nil key="textColor"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<textInputTraits key="textInputTraits" secureTextEntry="YES"/>
</textField>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="E0m-7s-XEz">
<rect key="frame" x="16" y="116" width="288" height="17"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="17" id="5fo-tK-QhJ"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="1" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="dWn-2T-XDN" secondAttribute="bottom" constant="8" id="ExW-Ye-ifb"/>
<constraint firstItem="E0m-7s-XEz" firstAttribute="top" secondItem="dWn-2T-XDN" secondAttribute="bottom" constant="4" id="PcG-Ss-0z5"/>
<constraint firstAttribute="trailing" secondItem="dWn-2T-XDN" secondAttribute="trailing" constant="16" id="TZI-tk-XCc"/>
<constraint firstItem="E0m-7s-XEz" firstAttribute="leading" secondItem="b0d-cJ-IJT" secondAttribute="leading" constant="16" id="VZb-a1-THP"/>
<constraint firstAttribute="trailing" secondItem="E0m-7s-XEz" secondAttribute="trailing" constant="16" id="bxv-dZ-6fV"/>
<constraint firstItem="dWn-2T-XDN" firstAttribute="top" secondItem="b0d-cJ-IJT" secondAttribute="top" constant="8" id="gXN-EW-HYY"/>
<constraint firstAttribute="bottom" secondItem="E0m-7s-XEz" secondAttribute="bottom" constant="8" id="vhj-er-C2E"/>
<constraint firstItem="dWn-2T-XDN" firstAttribute="leading" secondItem="b0d-cJ-IJT" secondAttribute="leading" constant="16" id="zAm-rb-vhR"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<connections>
<outlet property="errorMessageLabel" destination="E0m-7s-XEz" id="Xng-uT-cAm"/>
<outlet property="textField" destination="dWn-2T-XDN" id="xz6-aa-Izw"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="TableViewCellId" rowHeight="69" id="ExW-Pf-YtL" customClass="TextCell" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="0.0" y="296" width="320" height="69"/>
<rect key="frame" x="0.0" y="368" width="320" height="69"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="ExW-Pf-YtL" id="D4j-0H-Fhi">
<rect key="frame" x="0.0" y="0.0" width="320" height="69"/>
......@@ -308,7 +321,7 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="W0G-TV-Z9c" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1965.5999999999999" y="366.56671664167919"/>
<point key="canvasLocation" x="1965" y="366.25"/>
</scene>
<!--View Controller-->
<scene sceneID="c8H-6M-3dO">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment