Commit a44b205d authored by Kateryna Kostiuk's avatar Kateryna Kostiuk Committed by Thibault Wittemberg

navigation: apply the Coordinator pattern

this commit is about:
- split the storyboards into reusable components
- use the coordinator pattern the handle the navigation
- ease the dependancy injection
- refactor folders structure to match a "Features" pattern
- refactor the walkthrough UI

Change-Id: Idf67e8e7cee7ca7487d58073409fded654f4dc0d
parent f23abd39
This diff is collapsed.
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Edric Ladent-Milaret <edric.ladent-milaret@savoirfairelinux.com>
* Author: Romain Bertozzi <romain.bertozzi@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
......@@ -24,6 +25,7 @@ import RealmSwift
import SwiftyBeaver
import RxSwift
import Chameleon
import Contacts
import Contacts
......@@ -31,11 +33,22 @@ import Contacts
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static let daemonService = DaemonService(dRingAdaptor: DRingAdapter())
static let accountService = AccountsService(withAccountAdapter: AccountAdapter())
static let nameService = NameService(withNameRegistrationAdapter: NameRegistrationAdapter())
static let conversationsService = ConversationsService(withMessageAdapter: MessagesAdapter())
static let contactsService = ContactsService(withContactsAdapter: ContactsAdapter())
private let daemonService = DaemonService(dRingAdaptor: DRingAdapter())
private let accountService = AccountsService(withAccountAdapter: AccountAdapter())
private let nameService = NameService(withNameRegistrationAdapter: NameRegistrationAdapter())
private let conversationsService = ConversationsService(withMessageAdapter: MessagesAdapter())
private let contactsService = ContactsService(withContactsAdapter: ContactsAdapter())
public lazy var injectionBag: InjectionBag = {
return InjectionBag(withDaemonService: self.daemonService,
withAccountService: self.accountService,
withNameService: self.nameService,
withConversationService: self.conversationsService,
withContactsService: self.contactsService)
}()
private lazy var appCoordinator: AppCoordinator = {
return AppCoordinator(with: self.injectionBag)
}()
private let log = SwiftyBeaver.self
......@@ -50,29 +63,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
console.format = "$Dyyyy-MM-dd HH:mm:ss.SSS$d $C$L$c: $M"
log.addDestination(console)
// starts the daemon
SystemAdapter().registerConfigurationHandler()
self.startDaemon()
// themetize the app
Chameleon.setGlobalThemeUsingPrimaryColor(UIColor.ringMain, withSecondaryColor: UIColor.ringSecondary, andContentStyle: .light)
Chameleon.setRingThemeUsingPrimaryColor(UIColor.ringMain, withSecondaryColor: UIColor.ringSecondary, andContentStyle: .light)
self.loadAccounts()
// load accounts during splashscreen
// and ask the AppCoordinator to handle the first screen once loading is finished
self.accountService.loadAccounts().subscribe { [unowned self] (_) in
if let currentAccount = self.accountService.currentAccount {
self.contactsService.loadContacts(withAccount: currentAccount)
self.contactsService.loadContactRequests(withAccount: currentAccount)
}
self.window?.rootViewController = self.appCoordinator.rootViewController
self.window?.makeKeyAndVisible()
self.appCoordinator.start()
}.disposed(by: self.disposeBag)
return true
}
func applicationWillResignActive(_ application: UIApplication) {
}
func applicationDidEnterBackground(_ application: UIApplication) {
}
func applicationWillEnterForeground(_ application: UIApplication) {
}
func applicationDidBecomeActive(_ application: UIApplication) {
}
func applicationWillTerminate(_ application: UIApplication) {
self.stopDaemon()
}
......@@ -81,7 +94,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
fileprivate func startDaemon() {
do {
try AppDelegate.daemonService.startDaemon()
try self.daemonService.startDaemon()
} catch StartDaemonError.initializationFailure {
log.error("Daemon failed to initialize.")
} catch StartDaemonError.startFailure {
......@@ -95,42 +108,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
fileprivate func stopDaemon() {
do {
try AppDelegate.daemonService.stopDaemon()
try self.daemonService.stopDaemon()
} catch StopDaemonError.daemonNotRunning {
log.error("Daemon failed to stop because it was not already running.")
} catch {
log.error("Unknown error in Daemon stop.")
}
}
fileprivate func loadAccounts() {
AppDelegate.accountService.loadAccounts()
.subscribe(onSuccess: { (accountList: [AccountModel]) in
self.checkAccount(accountList: accountList)
}, onError: { _ in
self.presentWalkthrough()
}).disposed(by: disposeBag)
}
fileprivate func checkAccount(accountList: [AccountModel]) {
if accountList.isEmpty {
self.presentWalkthrough()
} else {
AppDelegate.contactsService.loadContacts(withAccount: AppDelegate.accountService.currentAccount!)
AppDelegate.contactsService.loadContactRequests(withAccount: AppDelegate.accountService.currentAccount!)
self.presentMainTabBar()
}
}
fileprivate func presentWalkthrough() {
let storyboard = UIStoryboard(name: "WalkthroughStoryboard", bundle: nil)
self.window?.rootViewController = storyboard.instantiateInitialViewController()
self.window?.makeKeyAndVisible()
}
fileprivate func presentMainTabBar() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
self.window?.rootViewController = storyboard.instantiateInitialViewController()
self.window?.makeKeyAndVisible()
}
}
......@@ -45,6 +45,7 @@ struct ColorAsset {
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
enum Asset {
static let backgroundRing = ImageAsset(name: "background_ring")
static let icContactPicture = ImageAsset(name: "ic_contact_picture")
static let logoRingBeta2Blanc = ImageAsset(name: "logo-ring-beta2-blanc")
......@@ -52,6 +53,7 @@ enum Asset {
static let allColors: [ColorAsset] = [
]
static let allImages: [ImageAsset] = [
backgroundRing,
icContactPicture,
logoRingBeta2Blanc,
]
......
......@@ -49,38 +49,60 @@ extension UIViewController {
// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name
enum StoryboardScene {
enum ContactRequestsViewController: StoryboardType {
static let storyboardName = "ContactRequestsViewController"
static let initialScene = InitialSceneType<Ring.ContactRequestsViewController>(storyboard: ContactRequestsViewController.self)
}
enum ConversationViewController: StoryboardType {
static let storyboardName = "ConversationViewController"
static let initialScene = InitialSceneType<Ring.ConversationViewController>(storyboard: ConversationViewController.self)
}
enum CreateAccountViewController: StoryboardType {
static let storyboardName = "CreateAccountViewController"
static let initialScene = InitialSceneType<Ring.CreateAccountViewController>(storyboard: CreateAccountViewController.self)
}
enum CreateProfileViewController: StoryboardType {
static let storyboardName = "CreateProfileViewController"
static let initialScene = InitialSceneType<Ring.CreateProfileViewController>(storyboard: CreateProfileViewController.self)
}
enum LaunchScreen: StoryboardType {
static let storyboardName = "LaunchScreen"
static let initialScene = InitialSceneType<UIViewController>(storyboard: LaunchScreen.self)
}
enum Main: StoryboardType {
static let storyboardName = "Main"
enum LinkDeviceViewController: StoryboardType {
static let storyboardName = "LinkDeviceViewController"
static let initialScene = InitialSceneType<Ring.MainTabBarViewController>(storyboard: Main.self)
static let initialScene = InitialSceneType<Ring.LinkDeviceViewController>(storyboard: LinkDeviceViewController.self)
}
enum MeDetailViewController: StoryboardType {
static let storyboardName = "MeDetailViewController"
static let mainStoryboard = SceneType<Ring.MainTabBarViewController>(storyboard: Main.self, identifier: "MainStoryboard")
static let initialScene = InitialSceneType<Ring.MeDetailViewController>(storyboard: MeDetailViewController.self)
}
enum WalkthroughStoryboard: StoryboardType {
static let storyboardName = "WalkthroughStoryboard"
enum MeViewController: StoryboardType {
static let storyboardName = "MeViewController"
static let initialScene = InitialSceneType<UINavigationController>(storyboard: WalkthroughStoryboard.self)
static let initialScene = InitialSceneType<Ring.MeViewController>(storyboard: MeViewController.self)
}
}
enum SmartlistViewController: StoryboardType {
static let storyboardName = "SmartlistViewController"
enum StoryboardSegue {
enum Main: String, SegueType {
case showMessages = "ShowMessages"
case accountDetails
static let initialScene = InitialSceneType<Ring.SmartlistViewController>(storyboard: SmartlistViewController.self)
}
enum WalkthroughStoryboard: String, SegueType {
case accountToPermissionsSegue = "AccountToPermissionsSegue"
case createProfileSegue = "CreateProfileSegue"
case linkDeviceToAccountSegue = "LinkDeviceToAccountSegue"
case profileToAccountSegue = "ProfileToAccountSegue"
case profileToLinkSegue = "ProfileToLinkSegue"
enum WelcomeViewController: StoryboardType {
static let storyboardName = "WelcomeViewController"
static let initialScene = InitialSceneType<Ring.WelcomeViewController>(storyboard: WelcomeViewController.self)
}
}
enum StoryboardSegue {
}
// swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name
private final class BundleToken {}
......@@ -29,31 +29,38 @@ enum L10n {
static let chooseStrongPassword = L10n.tr("Localizable", "createAccount.chooseStrongPassword")
/// Create your Ring account
static let createAccountFormTitle = L10n.tr("Localizable", "createAccount.createAccountFormTitle")
/// Enter new username
/// username
static let enterNewUsernamePlaceholder = L10n.tr("Localizable", "createAccount.enterNewUsernamePlaceholder")
/// Invalid username
/// invalid username
static let invalidUsername = L10n.tr("Localizable", "createAccount.invalidUsername")
/// Loading...
/// Loading
static let loading = L10n.tr("Localizable", "createAccount.loading")
/// Looking for username availability...
/// looking for username availability
static let lookingForUsernameAvailability = L10n.tr("Localizable", "createAccount.lookingForUsernameAvailability")
/// New Password
/// password
static let newPasswordPlaceholder = L10n.tr("Localizable", "createAccount.newPasswordPlaceholder")
/// 6 characters minimum
static let passwordCharactersNumberError = L10n.tr("Localizable", "createAccount.passwordCharactersNumberError")
/// Passwords do not match
/// passwords do not match
static let passwordNotMatchingError = L10n.tr("Localizable", "createAccount.passwordNotMatchingError")
/// Register public username (experimental)
static let registerPublicUsername = L10n.tr("Localizable", "createAccount.registerPublicUsername")
/// Repeat new password
/// confirm password
static let repeatPasswordPlaceholder = L10n.tr("Localizable", "createAccount.repeatPasswordPlaceholder")
/// Username already taken
/// username already taken
static let usernameAlreadyTaken = L10n.tr("Localizable", "createAccount.usernameAlreadyTaken")
/// Adding account
static let waitCreateAccountTitle = L10n.tr("Localizable", "createAccount.waitCreateAccountTitle")
}
enum Createprofile {
/// Skip to Create Account
static let createAccount = L10n.tr("Localizable", "createProfile.createAccount")
/// Skip to Link Device
static let linkDevice = L10n.tr("Localizable", "createProfile.linkDevice")
}
enum Global {
/// Invitations
static let contactRequestsTabBarTitle = L10n.tr("Localizable", "global.contactRequestsTabBarTitle")
/// Home
static let homeTabBarTitle = L10n.tr("Localizable", "global.homeTabBarTitle")
/// Me
......@@ -79,8 +86,8 @@ enum L10n {
/// Create a Ring account
static let createAccount = L10n.tr("Localizable", "welcome.createAccount")
/// Link this device to an account
static let linkDeviceButton = L10n.tr("Localizable", "welcome.linkDeviceButton")
/// A Ring account allows you to reach people securely in peer to peer through fully distributed network
static let linkDevice = L10n.tr("Localizable", "welcome.linkDevice")
/// Ring is a free and universal communication platform which preserves the users' privacy and freedoms
static let text = L10n.tr("Localizable", "welcome.text")
/// Welcome to Ring
static let title = L10n.tr("Localizable", "welcome.title")
......
/*
* Copyright (C) 2017 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
import UIKit
import RxSwift
/// Represents Application global navigation state
///
/// - needToOnboard: user has to onboard because he has no account
public enum AppState: State {
case needToOnboard
}
/// This Coordinator drives the global navigation of the app (presents the UITabBarController + popups the Walkthrough)
class AppCoordinator: Coordinator, StateableResponsive {
var rootViewController: UIViewController {
return self.tabBarViewController
}
var childCoordinators = [Coordinator]()
private let tabBarViewController = UITabBarController()
private let injectionBag: InjectionBag
let disposeBag = DisposeBag()
let stateSubject = PublishSubject<State>()
required init (with injectionBag: InjectionBag) {
self.injectionBag = injectionBag
self.stateSubject.subscribe(onNext: { [unowned self] (state) in
guard let state = state as? AppState else { return }
switch state {
case .needToOnboard:
self.showWalkthrough()
break
}
}).disposed(by: self.disposeBag)
}
func start () {
let conversationsCoordinator = ConversationsCoordinator(with: self.injectionBag)
let contactRequestsCoordinator = ContactRequestsCoordinator(with: self.injectionBag)
let meCoordinator = MeCoordinator(with: self.injectionBag)
self.tabBarViewController.viewControllers = [conversationsCoordinator.rootViewController, contactRequestsCoordinator.rootViewController, meCoordinator.rootViewController]
self.addChildCoordinator(childCoordinator: conversationsCoordinator)
self.addChildCoordinator(childCoordinator: contactRequestsCoordinator)
self.addChildCoordinator(childCoordinator: meCoordinator)
self.rootViewController.rx.viewDidAppear.take(1).subscribe(onNext: { [unowned self, unowned conversationsCoordinator, unowned contactRequestsCoordinator, unowned meCoordinator] (_) in
conversationsCoordinator.start()
contactRequestsCoordinator.start()
meCoordinator.start()
// show walkthrough if needed
if self.injectionBag.accountService.accounts.isEmpty {
self.stateSubject.onNext(AppState.needToOnboard)
}
}).disposed(by: self.disposeBag)
}
private func showWalkthrough () {
let walkthroughCoordinator = WalkthroughCoordinator(with: self.injectionBag)
self.addChildCoordinator(childCoordinator: walkthroughCoordinator)
let walkthroughViewController = walkthroughCoordinator.rootViewController
self.present(viewController: walkthroughViewController, withStyle: .popup, withAnimation: true)
walkthroughCoordinator.start()
walkthroughViewController.rx.viewDidDisappear.subscribe(onNext: { [weak self, weak walkthroughCoordinator] (_) in
walkthroughCoordinator?.stateSubject.dispose()
self?.removeChildCoordinator(childCoordinator: walkthroughCoordinator)
}).disposed(by: self.disposeBag)
}
}
/*
* Copyright (C) 2017 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,23 +18,27 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
import UIKit
import Reusable
import Foundation
class SwitchCell: UITableViewCell, NibReusable {
/// We can centralize in this bag every service that is to be used by every layer of the app
class InjectionBag {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var registerSwitch: UISwitch!
let daemonService: DaemonService
let accountService: AccountsService
let nameService: NameService
let conversationsService: ConversationsService
let contactsService: ContactsService
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
init (withDaemonService daemonService: DaemonService,
withAccountService accountService: AccountsService,
withNameService nameService: NameService,
withConversationService conversationService: ConversationsService,
withContactsService contactsService: ContactsService) {
self.daemonService = daemonService
self.accountService = accountService
self.nameService = nameService
self.conversationsService = conversationService
self.contactsService = contactsService
}
}
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com>
*
......
/*
* Copyright (C) 2017 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
import RxSwift
extension NotificationCenter {
static var keyboardHeight: Observable<CGFloat> {
return Observable
.from([
NotificationCenter.default.rx.notification(NSNotification.Name.UIKeyboardWillShow)
.map { notification -> CGFloat in
(notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0
},
NotificationCenter.default.rx.notification(NSNotification.Name.UIKeyboardWillHide)
.map { _ -> CGFloat in
0
}
])
.merge()
}
}
/*
* Copyright (C) 2016 Savoir-faire Linux Inc.
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com>
*
......
......@@ -21,6 +21,11 @@
import Foundation
import UIKit
private enum GradientAnchor {
case start
case end
}
extension UIView {
@IBInspectable
......@@ -72,4 +77,71 @@ extension UIView {
}
}
@IBInspectable
var gradientStartColor: UIColor {
get {
return self.retrieveGradientColor(for: .start)
}
set {
self.applyGradientColor(for: .start, with: newValue)
}
}
@IBInspectable
var gradientEndColor: UIColor {
get {
return self.retrieveGradientColor(for: .end)
}
set {
self.applyGradientColor(for: .end, with: newValue)
}
}
private func applyGradientColor(for anchor: GradientAnchor, with color: UIColor) {
if let layer = self.layer.sublayers?[0] as? CAGradientLayer {
// reuse the gradient layer that has already been set
if anchor == .start {
layer.colors = [color.cgColor, self.retrieveGradientColor(for: .end).cgColor]
} else {
layer.colors = [self.retrieveGradientColor(for: .start).cgColor, color.cgColor]
}
return
}
let layer = CAGradientLayer()
layer.frame = CGRect(origin: .zero, size: self.frame.size)
layer.startPoint = CGPoint(x: 0.5, y: 0)
layer.endPoint = CGPoint(x: 0.5, y: 1)
if anchor == .start {
layer.colors = [color.cgColor, self.retrieveGradientColor(for: .end).cgColor]
} else {
layer.colors = [self.retrieveGradientColor(for: .start).cgColor, color.cgColor]
}
layer.cornerRadius = self.cornerRadius
self.layer.addSublayer(layer)
}
private func retrieveGradientColor(for anchor: GradientAnchor) -> UIColor {
if let layer = self.layer.sublayers?[0] as? CAGradientLayer,
let colors = layer.colors as? [CGColor] {
if anchor == .start && !colors.isEmpty {
return UIColor(cgColor: colors[0])
}
if anchor == .end && colors.count >= 1 {
return UIColor(cgColor: colors[1])
}
return UIColor.clear
}
return UIColor.clear
}
}
/*
* Copyright (C) 2017 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
import UIKit
import RxSwift
extension UIViewController {
/// Find the active UITextField if it exists
///
/// - Parameters:
/// - view: The UIView to search into
/// - Returns: The active UITextField (ie: isFirstResponder)
func findActiveTextField(in view: UIView) -> UITextField? {
guard !view.subviews.isEmpty else { return nil }
for currentView in view.subviews {
if let textfield = currentView as? UITextField,
textfield.isFirstResponder {
return textfield
}
if let textField = findActiveTextField(in: currentView) {
return textField
}
}
return nil
}
/// Scroll the UIScrollView to the right position
/// according to keyboard's height
///
/// - Parameters:
/// - scrollView: The scrollView to adapt
/// - disposeBag: The RxSwift DisposeBag linked to the UIViewController life cycle
func adaptToKeyboardState (for scrollView: UIScrollView, with disposeBag: DisposeBag) {
NotificationCenter.keyboardHeight.observeOn(MainScheduler.instance).subscribe(onNext: { [unowned self, unowned scrollView] (height) in
let trueHeight = height>0 ? height+100 : 0.0
let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: trueHeight, right: 0.0)
scrollView.contentInset = contentInsets
// If active text field is hidden by keyboard, scroll it so it's visible
// Your app might not need or want this behavior.
if let activeField = self.findActiveTextField(in: scrollView) {
var aRect = self.view.frame
aRect.size.height -= trueHeight
if !aRect.contains(activeField.frame.origin) {
scrollView.scrollRectToVisible(activeField.frame, animated: true)
}
}
}).disposed(by: disposeBag)
}
}
/*
* Copyright (C) 2017 Savoir-faire Linux Inc.
*
* Author: Thibault Wittemberg <thibault.wittemberg@savoirfairelinux.com>
*
* This program is free software; you can redistribute it and/or modify