mirror of https://github.com/oxen-io/session-ios
Custom contact picker for invite flow
Preferred to the system contact picker because: 1. removes "group" clutter from header, unlikely to be used much. 2. can select while searching 3. fixes unified contact problem where e.g. If only one of your contact has a phone number, they appear disabled when choosing to invite via messaging, even though the other linked contact *does* have a phone number. 4. label users w/o email so it's clearer why they can't be selected Also: * Twitter share-image was too tall // FREEBIEpull/1/head
parent
f9a60b622d
commit
f30c733ef3
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
@ -0,0 +1,379 @@
|
||||
// Originally based on EPContacts
|
||||
//
|
||||
// Created by Prabaharan Elangovan on 12/10/15.
|
||||
// Parts Copyright © 2015 Prabaharan Elangovan. All rights reserved.
|
||||
//
|
||||
// Modified for Signal by Michael Kirk on 11/25/2016
|
||||
// Parts Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import Contacts
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
public protocol ContactsPickerDelegate {
|
||||
func contactsPicker(_: ContactsPicker, didContactFetchFailed error: NSError)
|
||||
func contactsPicker(_: ContactsPicker, didCancel error: NSError)
|
||||
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact)
|
||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact])
|
||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool
|
||||
}
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
public extension ContactsPickerDelegate {
|
||||
func contactsPicker(_: ContactsPicker, didContactFetchFailed error: NSError) { }
|
||||
func contactsPicker(_: ContactsPicker, didCancel error: NSError) { }
|
||||
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) { }
|
||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) { }
|
||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool { return true }
|
||||
}
|
||||
|
||||
public enum SubtitleCellValue{
|
||||
case phoneNumber
|
||||
case email
|
||||
}
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
open class ContactsPicker: UITableViewController, UISearchResultsUpdating, UISearchBarDelegate {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
let TAG = "[ContactsPicker]"
|
||||
let contactCellReuseIdentifier = "contactCellReuseIdentifier"
|
||||
let contactsManager: OWSContactsManager
|
||||
let collation = UILocalizedIndexedCollation.current()
|
||||
let contactStore = CNContactStore()
|
||||
lazy var resultSearchController = UISearchController()
|
||||
|
||||
// Data Source State
|
||||
lazy var sections = [[CNContact]]()
|
||||
lazy var filteredSections = [[CNContact]]()
|
||||
lazy var selectedContacts = [Contact]()
|
||||
|
||||
// Configuration
|
||||
open var contactsPickerDelegate: ContactsPickerDelegate?
|
||||
var subtitleCellValue = SubtitleCellValue.phoneNumber
|
||||
var multiSelectEnabled = false
|
||||
let allowedContactKeys: [CNKeyDescriptor] = [
|
||||
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
|
||||
CNContactThumbnailImageDataKey as CNKeyDescriptor,
|
||||
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
||||
CNContactEmailAddressesKey as CNKeyDescriptor
|
||||
]
|
||||
|
||||
// MARK: - Lifecycle Methods
|
||||
|
||||
override open func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
||||
|
||||
// Don't obscure table header (search bar) with table index
|
||||
tableView.sectionIndexBackgroundColor = UIColor.clear
|
||||
|
||||
// Auto size cells for dynamic type
|
||||
tableView.estimatedRowHeight = 60.0
|
||||
tableView.rowHeight = UITableViewAutomaticDimension
|
||||
|
||||
tableView.allowsMultipleSelection = multiSelectEnabled
|
||||
|
||||
registerContactCell()
|
||||
initializeBarButtons()
|
||||
reloadContacts()
|
||||
initializeSearchBar()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil)
|
||||
}
|
||||
|
||||
func didChangePreferredContentSize() {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
|
||||
func initializeSearchBar() {
|
||||
self.resultSearchController = ( {
|
||||
let controller = UISearchController(searchResultsController: nil)
|
||||
controller.searchResultsUpdater = self
|
||||
controller.dimsBackgroundDuringPresentation = false
|
||||
controller.searchBar.sizeToFit()
|
||||
controller.searchBar.delegate = self
|
||||
controller.hidesNavigationBarDuringPresentation = false
|
||||
|
||||
self.tableView.tableHeaderView = controller.searchBar
|
||||
return controller
|
||||
})()
|
||||
}
|
||||
|
||||
func initializeBarButtons() {
|
||||
let cancelButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.cancel, target: self, action: #selector(onTouchCancelButton))
|
||||
self.navigationItem.leftBarButtonItem = cancelButton
|
||||
|
||||
if multiSelectEnabled {
|
||||
let doneButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.done, target: self, action: #selector(onTouchDoneButton))
|
||||
self.navigationItem.rightBarButtonItem = doneButton
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func registerContactCell() {
|
||||
tableView.register(ContactCell.nib, forCellReuseIdentifier: contactCellReuseIdentifier)
|
||||
}
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
override init(style: UITableViewStyle) {
|
||||
contactsManager = Environment.getCurrent().contactsManager
|
||||
super.init(style: style)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
contactsManager = Environment.getCurrent().contactsManager
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
convenience public init(delegate: ContactsPickerDelegate?) {
|
||||
self.init(delegate: delegate, multiSelection: false)
|
||||
}
|
||||
|
||||
convenience public init(delegate: ContactsPickerDelegate?, multiSelection : Bool) {
|
||||
self.init(style: .plain)
|
||||
multiSelectEnabled = multiSelection
|
||||
contactsPickerDelegate = delegate
|
||||
}
|
||||
|
||||
convenience public init(delegate: ContactsPickerDelegate?, multiSelection : Bool, subtitleCellType: SubtitleCellValue) {
|
||||
self.init(style: .plain)
|
||||
multiSelectEnabled = multiSelection
|
||||
contactsPickerDelegate = delegate
|
||||
subtitleCellValue = subtitleCellType
|
||||
}
|
||||
|
||||
// MARK: - Contact Operations
|
||||
|
||||
open func reloadContacts() {
|
||||
getContacts( onError: { error in
|
||||
Logger.error("\(self.TAG) failed to reload contacts with error:\(error)")
|
||||
})
|
||||
}
|
||||
|
||||
func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) {
|
||||
switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) {
|
||||
case CNAuthorizationStatus.denied, CNAuthorizationStatus.restricted:
|
||||
|
||||
let title = NSLocalizedString("AB_PERMISSION_MISSING_TITLE", comment: "Alert title when contacts disabled")
|
||||
let body = NSLocalizedString("ADDRESSBOOK_RESTRICTED_ALERT_BODY", comment: "Alert body when contacts disabled")
|
||||
let alert = UIAlertController(title: title, message: body, preferredStyle: UIAlertControllerStyle.alert)
|
||||
|
||||
let dismissText = NSLocalizedString("DISMISS_BUTTON_TEXT", comment:"")
|
||||
|
||||
let okAction = UIAlertAction(title: dismissText, style: UIAlertActionStyle.default, handler: { action in
|
||||
let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"])
|
||||
self.contactsPickerDelegate?.contactsPicker(self, didContactFetchFailed: error)
|
||||
errorHandler(error)
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
})
|
||||
alert.addAction(okAction)
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
|
||||
case CNAuthorizationStatus.notDetermined:
|
||||
//This case means the user is prompted for the first time for allowing contacts
|
||||
contactStore.requestAccess(for: CNEntityType.contacts) { (granted, error) -> Void in
|
||||
//At this point an alert is provided to the user to provide access to contacts. This will get invoked if a user responds to the alert
|
||||
if granted {
|
||||
self.getContacts(onError: errorHandler)
|
||||
} else {
|
||||
errorHandler(error!)
|
||||
}
|
||||
}
|
||||
|
||||
case CNAuthorizationStatus.authorized:
|
||||
//Authorization granted by user for this app.
|
||||
var contacts = [CNContact]()
|
||||
|
||||
do {
|
||||
let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys)
|
||||
try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, stop) -> Void in
|
||||
contacts.append(contact)
|
||||
}
|
||||
self.sections = collatedContacts(contacts)
|
||||
self.tableView.reloadData()
|
||||
} catch let error as NSError {
|
||||
Logger.error("\(self.TAG) Failed to fetch contacts with error:\(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collatedContacts(_ contacts: [CNContact]) -> [[CNContact]] {
|
||||
let selector: Selector = #selector(getter: CNContact.nameForCollating)
|
||||
|
||||
var collated = Array(repeating: [CNContact](), count: collation.sectionTitles.count)
|
||||
for contact in contacts {
|
||||
let sectionNumber = collation.section(for: contact, collationStringSelector: selector)
|
||||
collated[sectionNumber].append(contact)
|
||||
}
|
||||
return collated
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Table View DataSource
|
||||
|
||||
override open func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return self.collation.sectionTitles.count
|
||||
}
|
||||
|
||||
override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let dataSource = resultSearchController.isActive ? filteredSections : sections
|
||||
|
||||
return dataSource[section].count
|
||||
}
|
||||
|
||||
// MARK: - Table View Delegates
|
||||
|
||||
override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as! ContactCell
|
||||
|
||||
let dataSource = resultSearchController.isActive ? filteredSections : sections
|
||||
let cnContact = dataSource[indexPath.section][indexPath.row]
|
||||
let contact = Contact(contact: cnContact)
|
||||
|
||||
cell.updateContactsinUI(contact, subtitleType: subtitleCellValue, contactsManager: self.contactsManager)
|
||||
let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId })
|
||||
cell.isSelected = isSelected
|
||||
|
||||
// Make sure we preserve selection across tableView.reloadData which happens when toggling between
|
||||
// search controller
|
||||
if (isSelected) {
|
||||
self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
||||
} else {
|
||||
self.tableView.deselectRow(at: indexPath, animated: false)
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
override open func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
|
||||
let cell = tableView.cellForRow(at: indexPath) as! ContactCell
|
||||
let deselectedContact = cell.contact!
|
||||
|
||||
selectedContacts = selectedContacts.filter() {
|
||||
return $0.uniqueId != deselectedContact.uniqueId
|
||||
}
|
||||
}
|
||||
|
||||
override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let cell = tableView.cellForRow(at: indexPath) as! ContactCell
|
||||
let selectedContact = cell.contact!
|
||||
|
||||
guard (contactsPickerDelegate == nil || contactsPickerDelegate!.contactsPicker(self, shouldSelectContact: selectedContact)) else {
|
||||
self.tableView.deselectRow(at: indexPath, animated: false)
|
||||
return
|
||||
}
|
||||
|
||||
selectedContacts.append(selectedContact)
|
||||
|
||||
if !multiSelectEnabled {
|
||||
//Single selection code
|
||||
resultSearchController.isActive = false
|
||||
self.dismiss(animated: true) {
|
||||
self.contactsPickerDelegate?.contactsPicker(self, didSelectContact: selectedContact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
|
||||
return collation.section(forSectionIndexTitle: index)
|
||||
}
|
||||
|
||||
override open func sectionIndexTitles(for tableView: UITableView) -> [String]? {
|
||||
return collation.sectionIndexTitles
|
||||
}
|
||||
|
||||
override open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
let dataSource = resultSearchController.isActive ? filteredSections : sections
|
||||
|
||||
if dataSource[section].count > 0 {
|
||||
return collation.sectionTitles[section]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Button Actions
|
||||
|
||||
func onTouchCancelButton() {
|
||||
contactsPickerDelegate?.contactsPicker(self, didCancel: NSError(domain: "contactsPickerErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey: "User Canceled Selection"]))
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func onTouchDoneButton() {
|
||||
contactsPickerDelegate?.contactsPicker(self, didSelectMultipleContacts: selectedContacts)
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: - Search Actions
|
||||
|
||||
open func updateSearchResults(for searchController: UISearchController) {
|
||||
if let searchText = resultSearchController.searchBar.text , searchController.isActive {
|
||||
|
||||
let predicate: NSPredicate
|
||||
if searchText.characters.count == 0 {
|
||||
filteredSections = sections
|
||||
} else {
|
||||
do {
|
||||
predicate = CNContact.predicateForContacts(matchingName: searchText)
|
||||
let filteredContacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: allowedContactKeys)
|
||||
filteredSections = collatedContacts(filteredContacts)
|
||||
} catch let error as NSError {
|
||||
Logger.error("\(self.TAG) updating search results failed with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
open func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
DispatchQueue.main.async {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
let ContactSortOrder = computeSortOrder()
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
func computeSortOrder() -> CNContactSortOrder {
|
||||
let comparator = CNContact.comparator(forNameSortOrder: .userDefault)
|
||||
|
||||
let contact0 = CNMutableContact()
|
||||
contact0.givenName = "A"
|
||||
contact0.familyName = "Z"
|
||||
|
||||
let contact1 = CNMutableContact()
|
||||
contact1.givenName = "Z"
|
||||
contact1.familyName = "A"
|
||||
|
||||
let result = comparator(contact0, contact1)
|
||||
|
||||
if result == .orderedAscending {
|
||||
return .givenName
|
||||
} else {
|
||||
return .familyName
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
fileprivate extension CNContact {
|
||||
/**
|
||||
* Sorting Key used by collation
|
||||
*/
|
||||
@objc var nameForCollating: String {
|
||||
get {
|
||||
let compositeName: String
|
||||
if ContactSortOrder == .familyName {
|
||||
compositeName = "\(self.familyName) \(self.givenName)"
|
||||
} else {
|
||||
compositeName = "\(self.givenName) \(self.familyName)"
|
||||
}
|
||||
return compositeName.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
// Originally based on EPContacts
|
||||
//
|
||||
// Created by Prabaharan Elangovan on 13/10/15.
|
||||
// Copyright © 2015 Prabaharan Elangovan. All rights reserved.
|
||||
//
|
||||
// Modified for Signal by Michael Kirk on 11/25/2016
|
||||
// Parts Copyright © 2016 Open Whisper Systems. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
class ContactCell: UITableViewCell {
|
||||
|
||||
static let nib = UINib(nibName:"ContactCell", bundle: nil)
|
||||
|
||||
@IBOutlet weak var contactTextLabel: UILabel!
|
||||
@IBOutlet weak var contactDetailTextLabel: UILabel!
|
||||
@IBOutlet weak var contactImageView: UIImageView!
|
||||
@IBOutlet weak var contactContainerView: UIView!
|
||||
|
||||
var contact: Contact?
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
// Initialization code
|
||||
selectionStyle = UITableViewCellSelectionStyle.none
|
||||
|
||||
contactContainerView.layer.masksToBounds = true
|
||||
contactContainerView.layer.cornerRadius = contactContainerView.frame.size.width/2
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil)
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
accessoryType = .none
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
accessoryType = selected ? .checkmark : .none
|
||||
}
|
||||
|
||||
func didChangePreferredContentSize() {
|
||||
contactTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
contactDetailTextLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
|
||||
}
|
||||
|
||||
func updateContactsinUI(_ contact: Contact, subtitleType: SubtitleCellValue, contactsManager: OWSContactsManager) {
|
||||
self.contact = contact
|
||||
|
||||
if contactTextLabel != nil {
|
||||
contactTextLabel.attributedText = contact.cnContact?.formattedFullName(font:contactTextLabel.font)
|
||||
}
|
||||
|
||||
updateSubtitleBasedonType(subtitleType, contact: contact)
|
||||
|
||||
if contact.image == nil {
|
||||
let contactIdForDeterminingBackgroundColor: String
|
||||
if let signalId = contact.parsedPhoneNumbers.first?.toE164() {
|
||||
contactIdForDeterminingBackgroundColor = signalId
|
||||
} else {
|
||||
contactIdForDeterminingBackgroundColor = contact.fullName
|
||||
}
|
||||
|
||||
let avatarBuilder = OWSContactAvatarBuilder(contactId:contactIdForDeterminingBackgroundColor,
|
||||
name:contact.fullName,
|
||||
contactsManager:contactsManager)
|
||||
self.contactImageView?.image = avatarBuilder.buildDefaultImage();
|
||||
} else {
|
||||
self.contactImageView?.image = contact.image
|
||||
}
|
||||
}
|
||||
|
||||
func updateSubtitleBasedonType(_ subtitleType: SubtitleCellValue , contact: Contact) {
|
||||
switch subtitleType {
|
||||
|
||||
case SubtitleCellValue.phoneNumber:
|
||||
if contact.userTextPhoneNumbers.count > 0 {
|
||||
self.contactDetailTextLabel.text = "\(contact.userTextPhoneNumbers[0])"
|
||||
} else {
|
||||
self.contactDetailTextLabel.text = NSLocalizedString("CONTACT_PICKER_NO_PHONE_NUMBERS_AVAILABLE", comment: "table cell subtitle when contact card has no known phone number")
|
||||
}
|
||||
case SubtitleCellValue.email:
|
||||
if contact.emails.count > 0 {
|
||||
self.contactDetailTextLabel.text = "\(contact.emails[0])"
|
||||
} else {
|
||||
self.contactDetailTextLabel.text = NSLocalizedString("CONTACT_PICKER_NO_EMAILS_AVAILABLE", comment: "table cell subtitle when contact card has no email")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 9.0, *)
|
||||
fileprivate extension CNContact {
|
||||
/**
|
||||
* Bold the sorting portion of the name. e.g. if we sort by family name, bold the family name.
|
||||
*/
|
||||
func formattedFullName(font: UIFont) -> NSAttributedString? {
|
||||
let keyToHighlight = ContactSortOrder == .familyName ? CNContactFamilyNameKey : CNContactGivenNameKey
|
||||
|
||||
if let attributedName = CNContactFormatter.attributedString(from: self, style: .fullName, defaultAttributes: nil) {
|
||||
let highlightedName = attributedName.mutableCopy() as! NSMutableAttributedString
|
||||
highlightedName.enumerateAttributes(in: NSMakeRange(0, highlightedName.length), options: [], using: { (attrs, range, stop) in
|
||||
if let property = attrs[CNContactPropertyAttribute] as? String, property == keyToHighlight {
|
||||
let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold)
|
||||
let boldAttributes = [
|
||||
NSFontAttributeName: UIFont(descriptor:boldDescriptor!, size: 0)
|
||||
]
|
||||
|
||||
highlightedName.addAttributes(boldAttributes, range: range)
|
||||
}
|
||||
})
|
||||
return highlightedName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11542" systemVersion="15G1108" 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="11524"/>
|
||||
<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" rowHeight="60" id="KGk-i7-Jjw" customClass="ContactCell" customModule="Signal" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="60"/>
|
||||
<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="320" height="59"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bKn-WF-9hk">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="59"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="59" id="z02-WI-o4t"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HUe-8f-NjY">
|
||||
<rect key="frame" x="60" y="10" width="250" height="21"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" red="0.33333333333333331" green="0.33333333333333331" blue="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tqx-Gt-ofy">
|
||||
<rect key="frame" x="60" y="35" width="234" height="18"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
|
||||
<color key="textColor" red="0.66666666666666663" green="0.66666666666666663" blue="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Bvt-tZ-NrJ">
|
||||
<rect key="frame" x="10" y="10" width="40" height="40"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mxT-DB-6lI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="40" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="0.7725490196" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="40" id="8v6-UT-2rU"/>
|
||||
<constraint firstAttribute="height" constant="40" id="pAq-lQ-W6t"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Bvt-tZ-NrJ" firstAttribute="centerY" secondItem="H2p-sc-9uM" secondAttribute="centerY" id="1V9-eI-crl"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Tqx-Gt-ofy" secondAttribute="trailing" constant="26" id="36W-0D-Qv3"/>
|
||||
<constraint firstItem="HUe-8f-NjY" firstAttribute="leading" secondItem="Bvt-tZ-NrJ" secondAttribute="trailing" constant="10" id="8Ws-h4-oJp"/>
|
||||
<constraint firstItem="bKn-WF-9hk" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="NDc-ZH-7qQ"/>
|
||||
<constraint firstItem="Tqx-Gt-ofy" firstAttribute="leading" secondItem="Bvt-tZ-NrJ" secondAttribute="trailing" constant="10" id="PDp-yF-F2b"/>
|
||||
<constraint firstItem="HUe-8f-NjY" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="10" id="R7t-dd-hcs"/>
|
||||
<constraint firstItem="Tqx-Gt-ofy" firstAttribute="top" secondItem="HUe-8f-NjY" secondAttribute="bottom" constant="4" id="aNx-bp-Uj5"/>
|
||||
<constraint firstAttribute="trailing" secondItem="HUe-8f-NjY" secondAttribute="trailing" constant="10" id="cOY-df-aFi"/>
|
||||
<constraint firstItem="Bvt-tZ-NrJ" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="10" id="m7q-1p-kjc"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Tqx-Gt-ofy" secondAttribute="bottom" constant="6" id="mEe-il-eMD"/>
|
||||
<constraint firstAttribute="bottom" secondItem="bKn-WF-9hk" secondAttribute="bottom" id="oHg-Fe-wLe"/>
|
||||
<constraint firstAttribute="trailing" secondItem="bKn-WF-9hk" secondAttribute="trailing" id="p0G-Qg-21x"/>
|
||||
<constraint firstItem="bKn-WF-9hk" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="q4R-qf-Bvi"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="contactContainerView" destination="Bvt-tZ-NrJ" id="qqg-f5-Xol"/>
|
||||
<outlet property="contactDetailTextLabel" destination="Tqx-Gt-ofy" id="Jlj-TK-UJl"/>
|
||||
<outlet property="contactImageView" destination="mxT-DB-6lI" id="aYl-FS-HAU"/>
|
||||
<outlet property="contactTextLabel" destination="HUe-8f-NjY" id="Smr-wZ-MHr"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="245" y="321"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
</document>
|
Binary file not shown.
Loading…
Reference in New Issue