Commit d28b7488 authored by Thibault Wittemberg's avatar Thibault Wittemberg

project: conform to a swifty way of coding

This commit:
- adapts code to a more Swifty way of coding
- uses previously added frameworks to make the code "type safer"

Change-Id: I5332e2843e82cac4f4f9af79714993863ef5963d
parent 69981855
disabled_rules: # rule identifiers to exclude from running
- todo
opt_in_rules: # some rules are only opt-in
- empty_count
......
This diff is collapsed.
......@@ -19,8 +19,9 @@
*/
import UIKit
import Reusable
class AccountTableViewCell: UITableViewCell {
class AccountTableViewCell: UITableViewCell, NibReusable {
// MARK: - Properties
@IBOutlet weak var activeSwitch: UISwitch!
......@@ -29,15 +30,6 @@ class AccountTableViewCell: UITableViewCell {
var account: AccountModel!
// MARK: - UITableViewCell
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
// MARK: - Actions
@IBAction func switchAccountState(_ sender: UISwitch) {
// account.isEnabled = sender.isOn
......
......@@ -140,17 +140,11 @@ class CreateRingAccountViewModel {
.asObservable().map ({ status in
switch status {
case .lookingUp:
return NSLocalizedString("LookingForUsernameAvailability",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
return L10n.Createaccount.lookingForUsernameAvailability.smartString
case .invalid:
return NSLocalizedString("InvalidUsername",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
return L10n.Createaccount.invalidUsername.smartString
case .alreadyTaken:
return NSLocalizedString("UsernameAlreadyTaken",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
return L10n.Createaccount.usernameAlreadyTaken.smartString
default:
return ""
}
......@@ -181,7 +175,7 @@ class CreateRingAccountViewModel {
//Loookup name request observer
self.username.asObservable().subscribe(onNext: { [unowned self] username in
self.nameService.lookupName(withAccount: "", nameserver: "", name: username)
}).addDisposableTo(disposeBag)
}).disposed(by: disposeBag)
//Name registration observer
self.accountService
......@@ -200,7 +194,7 @@ class CreateRingAccountViewModel {
name: self.username.value)
}
})
.addDisposableTo(disposeBag)
.disposed(by: disposeBag)
//Account creation state observer
self.accountService
......@@ -215,7 +209,7 @@ class CreateRingAccountViewModel {
}
}, onError: { _ in
self.accountCreationState.onError(AccountCreationError.unknown)
}).addDisposableTo(disposeBag)
}).disposed(by: disposeBag)
}
}
......@@ -238,34 +232,22 @@ extension AccountCreationError: LocalizedError {
var title: String {
switch self {
case .generic:
return NSLocalizedString("AccountCannotBeFoundTitle",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
return L10n.Alerts.accountCannotBeFoundTitle.smartString
case .network:
return NSLocalizedString("AccountNoNetworkTitle",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
return L10n.Alerts.accountNoNetworkTitle.smartString
default:
return NSLocalizedString("AccountDefaultErrorTitle",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
return L10n.Alerts.accountDefaultErrorTitle.smartString
}
}
var message: String {
switch self {
case .generic:
return NSLocalizedString("AcountCannotBeFoundMessage",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
return L10n.Alerts.accountDefaultErrorMessage.smartString
case .network:
return NSLocalizedString("AccountNoNetworkMessage",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
return L10n.Alerts.accountNoNetworkMessage.smartString
default:
return NSLocalizedString("AccountDefaultErrorMessage",
tableName: LocalizedStringTableNames.walkthrough,
comment: "")
return L10n.Alerts.accountDefaultErrorMessage.smartString
}
}
}
......@@ -20,9 +20,16 @@
import Foundation
/**
Time interval between TextField events in seconds
*/
let textFieldThrottlingDuration = 0.5
public enum Durations {
case textFieldThrottlingDuration
case alertFlashDuration
let alertFlashDuration = 1.0
var value: Double {
switch self {
case .textFieldThrottlingDuration:
return 0.5
case .alertFlashDuration:
return 1.0
}
}
}
......@@ -13,64 +13,83 @@ private class RingStringsBundleToken {}
// swiftlint:disable valid_docs
enum L10n {
enum Alerts {
/// Account Added
static let accountAddedTitle = L10n.tr("AccountAddedTitle")
static let accountAddedTitle = L10n.tr("alerts.accountAddedTitle")
/// Can't find account
static let accountCannotBeFoundTitle = L10n.tr("AccountCannotBeFoundTitle")
static let accountCannotBeFoundTitle = L10n.tr("alerts.accountCannotBeFoundTitle")
/// The account couldn't be created.
static let accountDefaultErrorMessage = L10n.tr("AccountDefaultErrorMessage")
static let accountDefaultErrorMessage = L10n.tr("alerts.accountDefaultErrorMessage")
/// Unknown error
static let accountDefaultErrorTitle = L10n.tr("AccountDefaultErrorTitle")
static let accountDefaultErrorTitle = L10n.tr("alerts.accountDefaultErrorTitle")
/// Could not add account because Ring couldn't connect to the distributed network. Check your device connectivity.
static let accountNoNetworkMessage = L10n.tr("AccountNoNetworkMessage")
static let accountNoNetworkMessage = L10n.tr("alerts.accountNoNetworkMessage")
/// Can't connect to the network
static let accountNoNetworkTitle = L10n.tr("AccountNoNetworkTitle")
static let accountNoNetworkTitle = L10n.tr("alerts.accountNoNetworkTitle")
/// Account couldn't be found on the Ring network. Make sure it was exported on Ring from an existing device, and that provided credentials are correct.
static let acountCannotBeFoundMessage = L10n.tr("AcountCannotBeFoundMessage")
static let acountCannotBeFoundMessage = L10n.tr("alerts.acountCannotBeFoundMessage")
}
enum Createaccount {
/// Choose strong password you will remember to protect your Ring account.
static let chooseStrongPassword = L10n.tr("ChooseStrongPassword")
/// Conversations
static let conversations = L10n.tr("Conversations")
/// Create a Ring account
static let createAccount = L10n.tr("CreateAccount")
static let chooseStrongPassword = L10n.tr("createAccount.chooseStrongPassword")
/// Create your Ring account
static let createAccountFormTitle = L10n.tr("CreateAccountFormTitle")
static let createAccountFormTitle = L10n.tr("createAccount.createAccountFormTitle")
/// Enter new username
static let enterNewUsernamePlaceholder = L10n.tr("EnterNewUsernamePlaceholder")
/// Home
static let homeTabBarTitle = L10n.tr("HomeTabBarTitle")
static let enterNewUsernamePlaceholder = L10n.tr("createAccount.enterNewUsernamePlaceholder")
/// Invalid username
static let invalidUsername = L10n.tr("InvalidUsername")
/// Link this device to an account
static let linkDeviceButton = L10n.tr("LinkDeviceButton")
static let invalidUsername = L10n.tr("createAccount.invalidUsername")
/// Loading...
static let loading = L10n.tr("createAccount.loading")
/// Looking for username availability...
static let lookingForUsernameAvailability = L10n.tr("LookingForUsernameAvailability")
static let lookingForUsernameAvailability = L10n.tr("createAccount.lookingForUsernameAvailability")
/// New Password
static let newPasswordPlaceholder = L10n.tr("NewPasswordPlaceholder")
/// No results
static let noResults = L10n.tr("NoResults")
static let newPasswordPlaceholder = L10n.tr("createAccount.newPasswordPlaceholder")
/// 6 characters minimum
static let passwordCharactersNumberError = L10n.tr("PasswordCharactersNumberError")
static let passwordCharactersNumberError = L10n.tr("createAccount.passwordCharactersNumberError")
/// Passwords do not match
static let passwordNotMatchingError = L10n.tr("PasswordNotMatchingError")
static let passwordNotMatchingError = L10n.tr("createAccount.passwordNotMatchingError")
/// Register public username (experimental)
static let registerPublicUsername = L10n.tr("RegisterPublicUsername")
static let registerPublicUsername = L10n.tr("createAccount.registerPublicUsername")
/// Repeat new password
static let repeatPasswordPlaceholder = L10n.tr("RepeatPasswordPlaceholder")
/// Searching...
static let searching = L10n.tr("Searching")
/// User found
static let userFound = L10n.tr("UserFound")
static let repeatPasswordPlaceholder = L10n.tr("createAccount.repeatPasswordPlaceholder")
/// Username already taken
static let usernameAlreadyTaken = L10n.tr("UsernameAlreadyTaken")
static let usernameAlreadyTaken = L10n.tr("createAccount.usernameAlreadyTaken")
/// Adding account
static let waitCreateAccountTitle = L10n.tr("WaitCreateAccountTitle")
static let waitCreateAccountTitle = L10n.tr("createAccount.waitCreateAccountTitle")
}
enum Global {
/// Home
static let homeTabBarTitle = L10n.tr("global.homeTabBarTitle")
/// Ok
static let ok = L10n.tr("global.ok")
}
enum Smartlist {
/// Conversations
static let conversations = L10n.tr("smartlist.conversations")
/// No results
static let noResults = L10n.tr("smartlist.noResults")
/// Searching...
static let searching = L10n.tr("smartlist.searching")
/// User found
static let userFound = L10n.tr("smartlist.userFound")
/// Yesterday
static let yesterday = L10n.tr("smartlist.yesterday")
}
enum Welcome {
/// Create a Ring account
static let createAccount = L10n.tr("welcome.createAccount")
/// Link this device to an account
static let linkDeviceButton = L10n.tr("welcome.linkDeviceButton")
/// A Ring account allows you to reach people securely in peer to peer through fully distributed network
static let welcomeText = L10n.tr("WelcomeText")
static let text = L10n.tr("welcome.text")
/// Welcome to Ring
static let welcomeTitle = L10n.tr("WelcomeTitle")
/// Yesterday
static let yesterday = L10n.tr("Yesterday")
static let title = L10n.tr("welcome.title")
}
}
struct LocalizableString {
......
......@@ -39,7 +39,7 @@ class ContactHelper {
} else {
userName.value = lookupNameResponse.address
}
}).addDisposableTo(disposeBag)
}).disposed(by: disposeBag)
nameService.lookupAddress(withAccount: "", nameserver: "", address: ringId)
......
......@@ -73,7 +73,7 @@ class ContactViewModel {
} else {
self.userName.value = lookupNameResponse.address
}
}).addDisposableTo(disposeBag)
}).disposed(by: disposeBag)
nameService.lookupAddress(withAccount: "", nameserver: "", address: self.contact.ringId)
}
......
......@@ -20,8 +20,9 @@
import UIKit
import RxSwift
import Reusable
class ConversationCell: UITableViewCell {
class ConversationCell: UITableViewCell, NibReusable {
@IBOutlet weak var profileImage: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
......@@ -30,11 +31,6 @@ class ConversationCell: UITableViewCell {
@IBOutlet weak var lastMessageDateLabel: UILabel!
@IBOutlet weak var lastMessagePreviewLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
self.newMessagesIndicator.backgroundColor = UIColor.red
......
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12120" systemVersion="16A323" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="ConversationCellId" rowHeight="76" id="KGk-i7-Jjw" customClass="ConversationCell" customModule="Ring" customModuleProvider="target">
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="76" id="KGk-i7-Jjw" customClass="ConversationCell" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="358" height="76"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="358" height="76"/>
<rect key="frame" x="0.0" y="0.0" width="358" height="75.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_contact_picture" translatesAutoresizingMaskIntoConstraints="NO" id="pFB-Jn-TNP">
......
......@@ -71,7 +71,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate {
}
func setupUI() {
self.viewModel?.userName.asObservable().bind(to: self.navigationItem.rx.title).addDisposableTo(disposeBag)
self.viewModel?.userName.asObservable().bind(to: self.navigationItem.rx.title).disposed(by: disposeBag)
self.tableView.contentInset.bottom = messageAccessoryView.frame.size.height
self.tableView.scrollIndicatorInsets.bottom = messageAccessoryView.frame.size.height
......@@ -97,22 +97,21 @@ class ConversationViewController: UIViewController, UITextFieldDelegate {
self.tableView.separatorStyle = .none
//Register cell
self.tableView.register(UINib.init(nibName: "MessageCell", bundle: nil),
forCellReuseIdentifier: "MessageCellId")
self.tableView.register(cellType: MessageCell.self)
//Bind the TableView to the ViewModel
self.viewModel?.messages
.bind(to: tableView.rx.items(cellIdentifier: "MessageCellId",
.bind(to: tableView.rx.items(cellIdentifier: "MessageCell",
cellType: MessageCell.self)) { _, messageViewModel, cell in
cell.messageLabel.text = messageViewModel.content
cell.bubblePosition = messageViewModel.bubblePosition()
}.addDisposableTo(disposeBag)
}.disposed(by: disposeBag)
//Scroll to bottom when reloaded
self.tableView.rx.methodInvoked(#selector(UITableView.reloadData)).subscribe(onNext: { _ in
self.scrollToBottomIfNeed()
self.updateBottomOffset()
}).addDisposableTo(disposeBag)
}).disposed(by: disposeBag)
}
fileprivate func updateBottomOffset() {
......@@ -153,7 +152,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate {
}
lazy var messageAccessoryView: MessageAccessoryView = {
return MessageAccessoryView.instanceFromNib()
return MessageAccessoryView.loadFromNib()
}()
func setupBindings() {
......@@ -162,7 +161,7 @@ class ConversationViewController: UIViewController, UITextFieldDelegate {
self.messageAccessoryView.messageTextField.rx.controlEvent(.editingDidEndOnExit).subscribe(onNext: { _ in
self.viewModel?.sendMessage(withContent: self.messageAccessoryView.messageTextField.text!)
self.messageAccessoryView.messageTextField.text = ""
}).addDisposableTo(disposeBag)
}).disposed(by: disposeBag)
}
// Avoid the keyboard to be hidden when the Send button is touched
......
......@@ -89,7 +89,7 @@ class ConversationViewModel {
self.log.error("Realm persistence with error: \(error)")
}
}).addDisposableTo(self.disposeBag)
}).disposed(by: self.disposeBag)
return tmp
}
......@@ -134,7 +134,7 @@ class ConversationViewModel {
if todayDay == day && todayMonth == month && todayYear == year {
return hourFormatter.string(from: lastMessageDate)
} else if day == todayDay - 1 {
return NSLocalizedString("Yesterday", tableName: "Smartlist", comment: "")
return L10n.Smartlist.yesterday.smartString
} else if todayYear == year && todayWeekOfYear == weekOfYear {
return lastMessageDate.dayOfWeek()
} else {
......@@ -158,7 +158,7 @@ class ConversationViewModel {
.subscribe(onCompleted: {
let accountHelper = AccountModelHelper(withAccount: self.accountService.currentAccount!)
self.saveMessage(withContent: content, byAuthor: accountHelper.ringId!, toConversationWith: (self.conversation.recipient?.ringId)!)
}).addDisposableTo(disposeBag)
}).disposed(by: disposeBag)
}
fileprivate func saveMessage(withContent content: String, byAuthor author: String, toConversationWith account: String) {
......@@ -167,7 +167,7 @@ class ConversationViewModel {
.subscribe(onCompleted: { [unowned self] in
self.log.debug("Message saved")
})
.addDisposableTo(disposeBag)
.disposed(by: disposeBag)
}
func setMessagesAsRead() {
......@@ -175,7 +175,7 @@ class ConversationViewModel {
.setMessagesAsRead(forConversation: self.conversation)
.subscribe(onCompleted: { [unowned self] in
self.log.debug("Message set as read")
}).addDisposableTo(disposeBag)
}).disposed(by: disposeBag)
}
fileprivate var unreadMessagesCount: Int {
......
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
*
* Author: Thibault Wittemberg <thibault.wittemberg@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
extension Bool {
func toString() -> String {
return self ? "true" : "false"
}
}
......@@ -22,22 +22,13 @@ import Foundation
extension String {
func toBool() -> Bool? {
switch self {
case "True", "true", "yes", "1":
switch self.lowercased() {
case "true", "yes", "1":
return true
case "False", "false", "no", "0":
case "false", "no", "0":
return false
default:
return nil
}
}
}
extension Bool {
func toString() -> String {
if self == true {
return "true"
}
return "false"
}
}
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
* Copyright (C) 2016 Savoir-faire Linux Inc.
*
* Author: Silbino Gonçalves Matado <silbino.gmatado@savoirfairelinux.com>
* Author: Thibault Wittemberg <thibault.wittemberg@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
......@@ -18,20 +18,14 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import Foundation
import UIKit
class RoundedTextField: UITextField {
extension UIColor {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.layer.borderColor = UIColor.white.cgColor
self.layer.borderWidth = 1.0
self.clipsToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
self.layer.cornerRadius = self.frame.size.height / 2.0
}
static let ringMain = UIColor(colorLiteralRed: 10.0/255.0,
green: 116.0/255.0,
blue: 137.0/255.0,
alpha: 1.0)
}
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
*
* Author: Edric Ladent-Milaret <edric.ladent-milaret@savoirfairelinux.com>
* Author: Thibault Wittemberg <thibault.wittemberg@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
......@@ -18,21 +18,58 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import Foundation
import UIKit
class RoundedButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
extension UIView {
//Button layout
self.layer.borderColor = self.backgroundColor?.cgColor
self.layer.borderWidth = 1.0
@IBInspectable
var cornerRadius: CGFloat {
get {
return self.layer.cornerRadius
}
set {
self.clipsToBounds = true
self.layer.cornerRadius = 15.0
self.contentEdgeInsets = UIEdgeInsets(top: 8.0, left: 8.0, bottom: 8.0, right: 8.0)
self.layer.cornerRadius = newValue
}
}
@IBInspectable
var roundedCorners: Bool {
get {
return self.cornerRadius == self.frame.height / 2
}
set {
if newValue {
self.cornerRadius = self.frame.height / 2
} else {
self.cornerRadius = 0
}
}
}
//Text colors
self.setTitleColor(UIColor.white, for: .normal)
self.setTitleColor(UIColor.gray, for: .disabled)
@IBInspectable
var borderWidth: CGFloat {
get {
return self.layer.borderWidth
}
set {
self.layer.borderWidth = newValue
}
}
@IBInspectable
var borderColor: UIColor {
get {
return UIColor(cgColor: self.layer.borderColor ?? UIColor.clear.cgColor)
}
set {
self.layer.borderColor = newValue.cgColor
}
}
}
......@@ -26,7 +26,7 @@ class MainTabBarViewController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
UITabBarItem.appearance()
.setTitleTextAttributes( [NSForegroundColorAttributeName: Colors.ringMainColor], for: .selected)
.setTitleTextAttributes( [NSForegroundColorAttributeName: UIColor.ringMain], for: .selected)
}
override func viewDidAppear(_ animated: Bool) {
......
......@@ -19,16 +19,10 @@
*/
import UIKit
import Reusable
class MessageAccessoryView: UIView {
class MessageAccessoryView: UIView, NibLoadable {
@IBOutlet weak var messageTextField: UITextField!
class func instanceFromNib() -> MessageAccessoryView {
guard let view = UINib(nibName: "MessageAccessoryView", bundle: nil).instantiate(withOwner: nil, options: nil).first as? MessageAccessoryView else {
fatalError("The view you are trying to instantiate is not a MessageAccessoryView")
}
return view
}
}
......@@ -19,13 +19,14 @@
*/
import UIKit
import Reusable
enum BubblePosition {
case received
case sent
}
class MessageCell: UITableViewCell {
class MessageCell: UITableViewCell, NibReusable {
@IBOutlet weak var bubble: UIView!
@IBOutlet weak var messageLabel: UILabel!
......@@ -44,7 +45,7 @@ class MessageCell: UITableViewCell {
self.containerLeadingConstraint.priority = 1
self.minimumLeadingConstraint.priority = 999
self.bubble.backgroundColor = Colors.ringMainColor
self.bubble.backgroundColor = UIColor.ringMain
self.messageLabel.textColor = UIColor.white
} else {
self.minimumLeadingConstraint.priority = 1
......
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="15G31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="MessageCellId" rowHeight="60" id="KGk-i7-Jjw" customClass="MessageCell" customModule="Ring" customModuleProvider="target">
<tableViewCell contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" rowHeight="60" id="KGk-i7-Jjw" customClass="MessageCell" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="510" height="47"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="510" height="46"/>
<rect key="frame" x="0.0" y="0.0" width="510" height="46.5"/>