Commit 8dfb5d62 authored by Kateryna Kostiuk's avatar Kateryna Kostiuk Committed by Andreas Traczyk

UI/UX: add contact page

Change-Id: I068ba7f45d88a44edbcbb4a6b241c4569c93fa40
Reviewed-by: Andreas Traczyk's avatarAndreas Traczyk <andreas.traczyk@savoirfairelinux.com>
parent ec8e54db
......@@ -8,3 +8,4 @@ github "andreamazz/AMPopTip"
github "ashleymills/Reachability.swift"
github "stephencelis/SQLite.swift" ~> 0.11.4
github "optonaut/ActiveLabel.swift"
github "gskbyte/GSKStretchyHeaderView"
......@@ -6,7 +6,8 @@ github "SwiftyBeaver/SwiftyBeaver" "1.4.3"
github "ViccAlexander/Chameleon" "2.2.0"
github "andreamazz/AMPopTip" "3.0.2"
github "ashleymills/Reachability.swift" "v4.1.0"
github "gskbyte/GSKStretchyHeaderView" "1.0.4"
github "optonaut/ActiveLabel.swift" "0.8.0"
github "pkluz/PKHUD" "5.0.0"
github "realm/realm-cocoa" "v3.0.1"
github "stephencelis/SQLite.swift" "0.11.4"
github "optonaut/ActiveLabel.swift" "0.8.0"
This diff is collapsed.
......@@ -40,3 +40,4 @@
#import "CallsAdapter.h"
#import <PushKit/PushKit.h>
#import <UserNotifications/UserNotifications.h>
#import <GSKStretchyHeaderView/GSKStretchyHeaderView.h>
......@@ -54,6 +54,7 @@ enum Asset {
static let backgroundRing = ImageAsset(name: "background_ring")
static let blockIcon = ImageAsset(name: "block_icon")
static let callButton = ImageAsset(name: "call_button")
static let clearConversation = ImageAsset(name: "clear_conversation")
static let contactRequestIcon = ImageAsset(name: "contact_request_icon")
static let conversationIcon = ImageAsset(name: "conversation_icon")
static let device = ImageAsset(name: "device")
......@@ -85,6 +86,7 @@ enum Asset {
backgroundRing,
blockIcon,
callButton,
clearConversation,
contactRequestIcon,
conversationIcon,
device,
......
......@@ -65,6 +65,11 @@ enum StoryboardScene {
static let initialScene = InitialSceneType<Ring.ContactRequestsViewController>(storyboard: ContactRequestsViewController.self)
}
enum ContactViewController: StoryboardType {
static let storyboardName = "ContactViewController"
static let initialScene = InitialSceneType<Ring.ContactViewController>(storyboard: ContactViewController.self)
}
enum ConversationViewController: StoryboardType {
static let storyboardName = "ConversationViewController"
......
......@@ -53,6 +53,8 @@ enum L10n {
static let confirmBlockContactTitle = L10n.tr("Localizable", "alerts.confirmBlockContactTitle")
/// Are you sure you want to delete this conversation permanently?
static let confirmDeleteConversation = L10n.tr("Localizable", "alerts.confirmDeleteConversation")
/// Are you sure you want to delete the conversation with this contact?
static let confirmDeleteConversationFromContact = L10n.tr("Localizable", "alerts.confirmDeleteConversationFromContact")
/// Delete Conversation
static let confirmDeleteConversationTitle = L10n.tr("Localizable", "alerts.confirmDeleteConversationTitle")
/// Please close application and try to open it again
......@@ -86,6 +88,19 @@ enum L10n {
static let unknown = L10n.tr("Localizable", "calls.unknown")
}
enum Contactpage {
/// Block Contact
static let blockContact = L10n.tr("Localizable", "contactPage.blockContact")
/// Clear Chat
static let clearConversation = L10n.tr("Localizable", "contactPage.clearConversation")
/// Send Message
static let sendMessage = L10n.tr("Localizable", "contactPage.sendMessage")
/// Start Audio Call
static let startAudioCall = L10n.tr("Localizable", "contactPage.startAudioCall")
/// Start Video Call
static let startVideoCall = L10n.tr("Localizable", "contactPage.startVideoCall")
}
enum Createaccount {
/// Choose strong password you will remember to protect your Ring account.
static let chooseStrongPassword = L10n.tr("Localizable", "createAccount.chooseStrongPassword")
......
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="4oO-a5-k5Y">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Contact View Controller-->
<scene sceneID="ufa-hq-gXE">
<objects>
<viewController id="4oO-a5-k5Y" customClass="ContactViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="l1v-RV-Jo2">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="150" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="Cjc-Dl-AsW">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="default" indentationWidth="0.0" reuseIdentifier="ProfileInfoCell" rowHeight="60" id="fHK-Bg-4wI">
<rect key="frame" x="0.0" y="28" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fHK-Bg-4wI" id="Eoe-sh-EbE">
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="CcM-C0-9Qc">
<rect key="frame" x="0.0" y="59" width="375" height="1"/>
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="Wce-19-zkv"/>
</constraints>
</view>
</subviews>
</tableViewCellContentView>
<constraints>
<constraint firstItem="CcM-C0-9Qc" firstAttribute="leading" secondItem="fHK-Bg-4wI" secondAttribute="leading" id="5vW-Qy-CMV"/>
<constraint firstAttribute="bottom" secondItem="CcM-C0-9Qc" secondAttribute="bottom" id="ED4-rl-PxK"/>
<constraint firstAttribute="trailing" secondItem="CcM-C0-9Qc" secondAttribute="trailing" id="XB6-gd-kTb"/>
</constraints>
</tableViewCell>
</prototypes>
</tableView>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="Cjc-Dl-AsW" firstAttribute="top" secondItem="l1v-RV-Jo2" secondAttribute="top" id="FCK-2Z-zav"/>
<constraint firstItem="Cjc-Dl-AsW" firstAttribute="leading" secondItem="cTQ-BN-ANe" secondAttribute="leading" id="JuN-WT-uvv"/>
<constraint firstItem="cTQ-BN-ANe" firstAttribute="trailing" secondItem="Cjc-Dl-AsW" secondAttribute="trailing" id="PIn-ZO-UAi"/>
<constraint firstItem="Cjc-Dl-AsW" firstAttribute="bottom" secondItem="cTQ-BN-ANe" secondAttribute="bottom" id="YSy-1B-Cde"/>
</constraints>
<viewLayoutGuide key="safeArea" id="cTQ-BN-ANe"/>
</view>
<connections>
<outlet property="tableView" destination="Cjc-Dl-AsW" id="NB3-A4-6sy"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="T10-Oq-bRM" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="132" y="-87.706146926536732"/>
</scene>
</scenes>
</document>
/*
* Copyright (C) 2018 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@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 UIKit
import Reusable
import RxSwift
import RxCocoa
import RxDataSources
import GSKStretchyHeaderView
class ContactViewController: UIViewController, StoryboardBased, ViewModelBased {
var viewModel: ContactViewModel!
@IBOutlet private weak var tableView: UITableView!
private let disposeBag = DisposeBag()
private let cellIdentifier = "ProfileInfoCell"
private var stretchyHeader: ProfileHeaderView!
private let titleView = TitleView(frame: CGRect(x: 0, y: 0, width: 200, height: 50))
override func viewDidLoad() {
super.viewDidLoad()
self.addHeaderView()
self.setUpTableView()
navigationItem.titleView = titleView
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
UIApplication.shared.statusBarStyle = .default
self.navigationController?.navigationBar.layer.shadowColor = UIColor.clear.cgColor
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
}
private func addHeaderView() {
guard let nibViews = Bundle.main.loadNibNamed("ProfileHeaderView",
owner: self,
options: nil) else {
return
}
guard let headerView = nibViews.first as? ProfileHeaderView else {
return
}
self.stretchyHeader = headerView
self.tableView.addSubview(self.stretchyHeader)
self.tableView.delegate = self
self.configureHeaderViewBinding()
}
private func configureHeaderViewBinding() {
// avatar
Observable<(Data?, String)>.combineLatest(self.viewModel.profileImageData.asObservable(),
self.viewModel.userName.asObservable()) { profileImage, username in
return (profileImage, username)
}
.observeOn(MainScheduler.instance)
.startWith((self.viewModel.profileImageData.value, self.viewModel.userName.value))
.subscribe({ [weak self] profileData -> Void in
self?.stretchyHeader.avatarView?.subviews.forEach({ $0.removeFromSuperview() })
self?.stretchyHeader.avatarView?.addSubview(AvatarView(profileImageData: profileData.element?.0,
username: (profileData.element?.1)!,
size: 100))
self?.titleView.avatarImage = AvatarView(profileImageData: profileData.element?.0,
username: (profileData.element?.1)!,
size: 36)
return
})
.disposed(by: self.disposeBag)
self.viewModel.userName.asDriver()
.drive(self.stretchyHeader.userName.rx.text)
.disposed(by: self.disposeBag)
self.viewModel.displayName.asDriver()
.drive(self.stretchyHeader.displayName.rx.text)
.disposed(by: self.disposeBag)
self.viewModel.titleName
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] name in
self?.titleView.text = name
}).disposed(by: self.disposeBag)
}
private func setUpTableView() {
self.tableView.rowHeight = 60.0
let configureCell: (TableViewSectionedDataSource, UITableView, IndexPath, SectionModel<String, ContactActions>.Item)
-> UITableViewCell = {
(dataSource: TableViewSectionedDataSource<SectionModel<String, ContactActions>>,
tableView: UITableView,
indexPath: IndexPath,
conversationItem: SectionModel<String, ContactActions>.Item) in
let model = dataSource.sectionModels
if model[indexPath.section].model == self.cellIdentifier {
let cell = tableView.dequeueReusableCell(withIdentifier: self.cellIdentifier)
let image = UIImage(asset: conversationItem.image)
let tintedImage = image?.withRenderingMode(.alwaysTemplate)
cell?.imageView?.image = tintedImage
cell?.imageView?.tintColor = UIColor.ringSecondary
cell?.textLabel?.text = conversationItem.title
return cell!
}
return UITableViewCell()
}
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String,
ContactActions>>(configureCell: configureCell)
self.viewModel.tableSection
.observeOn(MainScheduler.instance)
.bind(to: self.tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
self.tableView.rx.itemSelected
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] indexPath in
if self?.tableView.cellForRow(at: indexPath) != nil {
switch indexPath.row {
case 0:
self?.viewModel.startAudioCall()
case 1:
self?.viewModel.startCall()
case 2:
_ = self?.navigationController?.popViewController(animated: false)
case 3:
self?.showDeleteConversationConfirmation()
case 4:
self?.showBlockContactConfirmation()
default:
break
}
self?.tableView.deselectRow(at: indexPath, animated: true)
}
}).disposed(by: self.disposeBag)
}
private func showDeleteConversationConfirmation() {
let alert = UIAlertController(title: L10n.Alerts.confirmDeleteConversationTitle, message: L10n.Alerts.confirmDeleteConversationFromContact, preferredStyle: .alert)
let deleteAction = UIAlertAction(title: L10n.Actions.deleteAction, style: .destructive) { [weak self](_: UIAlertAction!) -> Void in
self?.viewModel.deleteConversation()
}
let cancelAction = UIAlertAction(title: L10n.Actions.cancelAction, style: .default) { (_: UIAlertAction!) -> Void in }
alert.addAction(deleteAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
private func showBlockContactConfirmation() {
let alert = UIAlertController(title: L10n.Alerts.confirmBlockContactTitle, message: L10n.Alerts.confirmBlockContact, preferredStyle: .alert)
let blockAction = UIAlertAction(title: L10n.Actions.blockAction, style: .destructive) { [weak self] (_: UIAlertAction!) -> Void in
self?.viewModel.blockContact()
_ = self?.navigationController?.popToRootViewController(animated: false)
}
let cancelAction = UIAlertAction(title: L10n.Actions.cancelAction, style: .default) { (_: UIAlertAction!) -> Void in }
alert.addAction(blockAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
}
extension ContactViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let navigationHeight = self.navigationController?.navigationBar.bounds.height
var size = self.view.bounds.size
var titlViewThreshold: CGFloat = 0
if let height = navigationHeight {
size.height -= (height - 10)
titlViewThreshold = height
}
if scrollView.contentSize.height < size.height {
scrollView.contentSize = size
}
guard let titleView = navigationItem.titleView as? TitleView else { return }
titleView.scrollViewDidScroll(scrollView, threshold: titlViewThreshold)
}
}
/*
* Copyright (C) 2018 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@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 UIKit
import Reusable
import RxSwift
import RxCocoa
import RxDataSources
struct ContactActions {
let title: String
let image: ImageAsset
}
class ContactViewModel: ViewModel, Stateable {
private let disposeBag = DisposeBag()
// MARK: - Rx Stateable
private let stateSubject = PublishSubject<State>()
lazy var state: Observable<State> = {
return self.stateSubject.asObservable()
}()
private let contactService: ContactsService
private let profileService: ProfilesService
private let accountService: AccountsService
private let conversationService: ConversationsService
let tableSection = Observable<[SectionModel<String, ContactActions>]>
.just([SectionModel(model: "ProfileInfoCell",
items:
[ ContactActions(title: L10n.Contactpage.startAudioCall, image: Asset.callButton),
ContactActions(title: L10n.Contactpage.startVideoCall, image: Asset.videoRunning),
ContactActions(title: L10n.Contactpage.sendMessage, image: Asset.conversationIcon),
ContactActions(title: L10n.Contactpage.clearConversation, image: Asset.clearConversation),
ContactActions(title: L10n.Contactpage.blockContact, image: Asset.blockIcon)])])
var conversation: ConversationModel! {
didSet {
self.userName.value = conversation.recipientRingId
if let profile = conversation.participantProfile, let alias = profile.alias, !alias.isEmpty {
self.displayName.value = alias
}
if let contact = self.contactService.contact(withRingId: conversation.recipientRingId),
let name = contact.userName {
self.userName.value = name
}
self.contactService
.getContactRequestVCard(forContactWithRingId: conversation.recipientRingId)
.subscribe(onSuccess: { [unowned self] vCard in
if !VCardUtils.getName(from: vCard).isEmpty {
self.displayName.value = VCardUtils.getName(from: vCard)
}
guard let imageData = vCard.imageData else {
return
}
self.profileImageData.value = imageData
})
.disposed(by: self.disposeBag)
self.profileService.getProfile(ringId: conversation.recipientRingId,
createIfNotexists: false)
.subscribe(onNext: { [unowned self] profile in
if let alias = profile.alias, !alias.isEmpty {
self.displayName.value = alias
}
if let photo = profile.photo,
let data = NSData(base64Encoded: photo, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters) as Data? {
self.profileImageData.value = data
}
}).disposed(by: disposeBag)
}
}
var userName = Variable<String>("")
var displayName = Variable<String>("")
lazy var titleName: Observable<String> = {
return Observable.combineLatest(userName.asObservable(),
displayName.asObservable()) {(userName, displayname) in
if displayname.isEmpty {
return userName
}
return displayname
}
}()
var profileImageData = Variable<Data?>(nil)
required init (with injectionBag: InjectionBag) {
self.contactService = injectionBag.contactsService
self.profileService = injectionBag.profileService
self.accountService = injectionBag.accountService
self.conversationService = injectionBag.conversationsService
}
func startCall() {
self.stateSubject.onNext(ConversationState
.startCall(contactRingId: conversation.recipientRingId,
userName: self.userName.value))
}
func startAudioCall() {
self.stateSubject.onNext(ConversationState
.startAudioCall(contactRingId: conversation.recipientRingId,
userName: self.userName.value))
}
func deleteConversation() {
self.conversationService
.deleteConversation(conversation: conversation,
keepContactInteraction: true)
}
func blockContact() {
let contactRingId = conversation.recipientRingId
let accountId = conversation.accountId
let removeCompleted = self.contactService.removeContact(withRingId: contactRingId,
ban: true,
withAccountId: accountId)
removeCompleted.asObservable()
.subscribe(onCompleted: { [unowned self] in
self.conversationService
.deleteConversation(conversation: self.conversation,
keepContactInteraction: false)
}).disposed(by: self.disposeBag)
}
}
/*
* Copyright (C) 2018 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@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.
*/
class ProfileHeaderView: GSKStretchyHeaderView {
@IBOutlet var avatarView: UIView!
@IBOutlet var displayName: UILabel!
@IBOutlet var userName: UILabel!
override func didChangeStretchFactor(_ stretchFactor: CGFloat) {
var alpha = CGFloatTranslateRange(stretchFactor, 0.2, 0.7, 0, 1)
alpha = max(0, min(1, alpha))
self.avatarView.alpha = alpha
self.displayName.alpha = alpha
self.userName.alpha = alpha
var scale = CGFloatTranslateRange(stretchFactor, 0.1, 0.9, 0.6, 1)
scale = max(0.4, min(1, scale))
self.avatarView.transform = CGAffineTransform(scaleX: scale, y: scale)
self.displayName.transform = CGAffineTransform(scaleX: scale, y: scale)
self.userName.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<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"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ProfileHeaderView" customModule="Ring" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="260"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="qRJ-Vm-D2L">
<rect key="frame" x="20" y="45" width="335" height="170"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="O5s-f8-qTg">
<rect key="frame" x="117" y="0.0" width="100" height="100"/>
<constraints>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="100" id="0bW-FE-CyE"/>
<constraint firstAttribute="width" constant="100" id="3zC-80-Zmb"/>
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="XKE-QU-aa4"/>
<constraint firstAttribute="height" constant="100" id="tVC-6H-lWE"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pXf-5b-Ea7">
<rect key="frame" x="20" y="115" width="295" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RXO-IT-XHT">
<rect key="frame" x="20" y="160" width="295" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="pXf-5b-Ea7" firstAttribute="leading" secondItem="qRJ-Vm-D2L" secondAttribute="leading" constant="20" id="4GL-1G-aVa"/>
<constraint firstItem="RXO-IT-XHT" firstAttribute="centerX" secondItem="qRJ-Vm-D2L" secondAttribute="centerX" id="5bW-wu-qSP"/>
<constraint firstItem="RXO-IT-XHT" firstAttribute="leading" secondItem="qRJ-Vm-D2L" secondAttribute="leading" constant="20" id="7h7-Wy-gTw"/>
<constraint firstAttribute="trailing" secondItem="RXO-IT-XHT" secondAttribute="trailing" constant="20" id="HQj-9p-TOG"/>
<constraint firstAttribute="height" constant="170" id="IGG-hj-xx5"/>
<constraint firstAttribute="bottom" secondItem="RXO-IT-XHT" secondAttribute="bottom" constant="10" id="JKK-IM-lXC"/>
<constraint firstAttribute="trailing" secondItem="pXf-5b-Ea7" secondAttribute="trailing" constant="20" id="PXK-mj-eqh"/>
<constraint firstItem="O5s-f8-qTg" firstAttribute="top" secondItem="qRJ-Vm-D2L" secondAttribute="top" id="S3h-dl-BQi"/>
<constraint firstItem="O5s-f8-qTg" firstAttribute="centerX" secondItem="qRJ-Vm-D2L" secondAttribute="centerX" id="dT6-k6-lz6"/>
<constraint firstItem="pXf-5b-Ea7" firstAttribute="top" secondItem="O5s-f8-qTg" secondAttribute="bottom" constant="15" id="kqv-KA-ZhV"/>
<constraint firstItem="pXf-5b-Ea7" firstAttribute="centerX" secondItem="qRJ-Vm-D2L" secondAttribute="centerX" id="pl2-GO-6oz"/>
</constraints>
</view>
</subviews>
<color key="backgroundColor" red="0.92156862745098034" green="0.92156862745098034" blue="0.92156862745098034" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="qRJ-Vm-D2L" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="0b8-kP-p8b"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="qRJ-Vm-D2L" secondAttribute="trailing" constant="20" id="Gh1-2s-W0F"/>
<constraint firstItem="qRJ-Vm-D2L" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="20" id="uDA-n9-stu"/>
<constraint firstItem="qRJ-Vm-D2L" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="yT7-Df-IsD"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="maximumContentHeight">
<real key="value" value="260"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
<connections>
<outlet property="avatarView" destination="O5s-f8-qTg" id="37V-MG-dre"/>
<outlet property="displayName" destination="pXf-5b-Ea7" id="XwT-cN-xyc"/>
<outlet property="userName" destination="RXO-IT-XHT" id="tuJ-tQ-OQZ"/>
</connections>
</view>
</objects>
</document>
/*
* Copyright (C) 2018 Savoir-faire Linux Inc.
*
* Author: Kateryna Kostiuk <kateryna.kostiuk@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 UIKit
import RxSwift
final class TitleView: UIView {
private let containerView = UIView()
private let avatarView = UIView()
private let label = UILabel()
private var contentOffset: CGFloat = 0 {
didSet {
containerView.frame.origin.y = titleVerticalPositionAdjusted(by: contentOffset)
let midY = bounds.midY - containerView.bounds.height * 0.5
let value = max(bounds.maxY - contentOffset, midY).rounded()
let alpha = midY * (1 / value)
containerView.alpha = alpha
}
}
var avatarImage: UIView = UIView() {
didSet {
avatarView.subviews.forEach({ $0.removeFromSuperview() })
avatarView.addSubview(avatarImage)
self.layoutSubviews()
}
}
var text: String = "" {
didSet {
label.text = text
self.layoutSubviews()
}
}
// MARK: Initializers
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(containerView)
containerView.addSubview(avatarView)
containerView.addSubview(label)
label.textColor = UIColor.ringSecondary
label.font = UIFont.systemFont(ofSize: 17.0, weight: .regular)
clipsToBounds = true
isUserInteractionEnabled = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
sizeToFit()
}
private func layoutSubviewsBefore11() {
let margin: CGFloat = 10
guard let navBar = typedSuperview() as? UINavigationBar else { return }
let center = convert(navBar.center, from: navBar)
let sizeLabel = label.sizeThatFits(bounds.size)