weareopensource/Swift

View on GitHub
waosSwift/modules/users/controllers/UserController.swift

Summary

Maintainability
A
3 hrs
Test Coverage
/**
 * Dependencies
 */

import UIKit
import RxSwift
import ReactorKit
import Eureka
import SafariServices
import FontAwesome
import MessageUI
import SwiftMessages
import RxAppState

/**
 * Controller
 */

class UserController: CoreFormController, View {

    // MARK: UI

    let refreshControl = CoreUIRefreshControl()

    let imageAvatar = UIImageView().then {
        $0.contentMode = UIView.ContentMode.scaleAspectFill
        $0.layer.masksToBounds = true
        $0.backgroundColor = UIColor.lightGray.withAlphaComponent(0.25)
        $0.kf.indicatorType = .activity
        $0.layer.cornerRadius = Metric.avatar/2
    }
    let labelName = CoreUILabel().then {
        $0.textAlignment = .center
    }
    let buttonInfo = UIButton().then {
        $0.setTitleColor(Metric.secondary, for: .normal)
        $0.backgroundColor = Metric.background ?? .white
    }

    // buttons profil
    let buttonProfil = ButtonRow {
        $0.title = L10n.userEdit
        $0.setFontAwesomeIcon("fa-user")
    }
    let buttonPreferences = ButtonRow {
        $0.title = L10n.userPreferences
        $0.setFontAwesomeIcon("fa-drafting-compass")
    }

    // buttons App
    let buttonSite = ButtonRow {
        $0.title = L10n.userSite
        $0.setFontAwesomeIcon("fa-external-link-alt", color: Metric.secondary ?? .lightGray, opacity: 1)
    }
    let buttonBlog = ButtonRow {
        $0.title = L10n.userBlog
        $0.setFontAwesomeIcon("fa-rss", color: Metric.secondary ?? .lightGray, opacity: 1)
    }
    let buttonUs = ButtonRow {
        $0.title = L10n.userUs
        $0.setFontAwesomeIcon("fa-user-astronaut")
    }
    let buttonSupport = ButtonRow {
        $0.title = L10n.userSupport
        $0.setFontAwesomeIcon("fa-question")
    }
    let buttonMore = ButtonRow {
        $0.title = L10n.userMore
        $0.setFontAwesomeIcon("fa-ellipsis-h")
    }

    // buttons social Networks
    let buttonInstagram = ButtonRow {
        $0.title = "Instagram"
        $0.setFontAwesomeIcon("fa-instagram", style: .brands, color: Metric.instagram ?? .lightGray, opacity: 1)
    }
    let buttonTwitter = ButtonRow {
        $0.title = "Twitter"
        $0.setFontAwesomeIcon("fa-twitter", style: .brands, color: Metric.twitter ?? .lightGray, opacity: 1)
    }
    let buttonLinkedin = ButtonRow {
        $0.title = "Linkedin"
        $0.setFontAwesomeIcon("fa-linkedin", style: .brands, color: Metric.linkedin ?? .lightGray, opacity: 1)
    }
    let buttonFacebook = ButtonRow {
        $0.title = "Facebook"
        $0.setFontAwesomeIcon("fa-facebook", style: .brands, color: Metric.facebook ?? .lightGray, opacity: 1)
    }

    // buttons Actions
    let buttonReport = ButtonRow {
        $0.title = L10n.userReport
        $0.setFontAwesomeIcon("fa-bug")
    }
    let buttonContact = ButtonRow {
        $0.title = L10n.userContact
        $0.setFontAwesomeIcon("fa-envelope")
    }
    let buttonData = ButtonRow {
        $0.title = L10n.userData
        $0.setFontAwesomeIcon("fa-database")
    }
    let buttonLogout = ButtonRow {
        $0.title = L10n.userLogout
        $0.setFontAwesomeIcon("fa-arrow-left")
    }
    let buttonDelete = ButtonRow {
        $0.title = L10n.userDelete
        $0.setFontAwesomeIcon("fa-trash-alt", color: Metric.error ?? .red, opacity: 1)
    }

    // MARK: Properties

    let application = UIApplication.shared

    // MARK: Initializing

    init(reactor: UserReactor) {
        super.init()
        self.reactor = reactor
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        form
            +++ Section { section in
                var header = HeaderFooterView<UIButton>(.class)
                header.height = { 190 }
                header.onSetupView = {view, _ in
                    view.addSubview(self.imageAvatar)
                    view.addSubview(self.labelName)
                    view.addSubview(self.buttonInfo)
                    self.imageAvatar.snp.makeConstraints { (make) -> Void in
                        make.width.height.equalTo(Metric.avatar)
                        make.centerX.equalTo(view)
                        make.top.equalTo(view).offset(Metric.margin*1.2)
                    }
                    self.labelName.snp.makeConstraints { (make) -> Void in
                        make.centerX.equalTo(view)
                        make.top.equalTo(self.imageAvatar.snp.bottom).offset(Metric.margin/2)
                    }
                    self.buttonInfo.snp.makeConstraints { (make) -> Void in
                        make.right.left.equalTo(view)
                        make.top.equalTo(self.labelName.snp.bottom)
                    }
                }
                section.header = header
            }
            <<< self.buttonProfil
            <<< self.buttonPreferences
            +++ Section(header: L10n.userSectionApp, footer: "")
            <<< self.buttonBlog
            <<< self.buttonSite
            <<< self.buttonUs
            <<< self.buttonSupport
            <<< self.buttonMore
            +++ Section(header: L10n.userSectionSocialnetworks, footer: "")
            <<< self.buttonInstagram
            <<< self.buttonTwitter
            <<< self.buttonLinkedin
            <<< self.buttonFacebook
            +++ Section(header: L10n.userSectionContact, footer: "")
            <<< self.buttonContact
            <<< self.buttonReport
            <<< self.buttonData
            +++ Section(header: L10n.userSectionActions, footer: "")
            <<< self.buttonLogout
            <<< self.buttonDelete

        self.tableView.refreshControl = refreshControl
        self.tableView.setContentOffset(CGPoint(x: 0, y: -refreshControl.frame.size.height), animated: true)
        self.view.addSubview(self.tableView)
    }

    // MARK: Binding

    func bind(reactor: UserReactor) {
        bindView(reactor)
        bindAction(reactor)
        bindState(reactor)
    }

}

/**
 * Extensions
 */

private extension UserController {

    // MARK: views (View -> View)

    func bindView(_ reactor: UserReactor) {
        // buttons
        self.buttonProfil.rx.tap
            .subscribe(onNext: { _ in
                let viewController = UserViewController(reactor: reactor.editReactor(reactor.currentState.user))
                let navigationController = UINavigationController(rootViewController: viewController)
                self.present(navigationController, animated: true, completion: nil)
            })
            .disposed(by: disposeBag)
        self.buttonPreferences.rx.tap
            .subscribe(onNext: { _ in
                let viewController = UserPreferenceController(reactor: reactor.preferenceReactor(reactor.currentState.user, reactor.currentState.policy))
                let navigationController = UINavigationController(rootViewController: viewController)
                self.present(navigationController, animated: true, completion: nil)
            })
            .disposed(by: disposeBag)
        // app
        self.buttonBlog.rx.tap
            .subscribe(onNext: { _ in
                guard let url = URL(string: (config["app"]["links"]["blog"].string ?? "")) else { return }
                let svc = SFSafariViewController(url: url)
                self.present(svc, animated: true, completion: nil)
            })
            .disposed(by: disposeBag)
        self.buttonSite.rx.tap
            .subscribe(onNext: { _ in
                guard let url = URL(string: (config["app"]["links"]["site"].string ?? "")) else { return }
                let svc = SFSafariViewController(url: url)
                self.present(svc, animated: true, completion: nil)
            })
            .disposed(by: disposeBag)
        self.buttonUs.rx.tap
            .subscribe(onNext: { _ in
                guard let url = URL(string: (config["app"]["links"]["us"].string ?? "")) else { return }
                let svc = SFSafariViewController(url: url)
                self.present(svc, animated: true, completion: nil)
            })
            .disposed(by: disposeBag)
        self.buttonSupport.rx.tap
            .subscribe(onNext: { _ in
                if (L10n.linksSupport.prefix(4) == "http") {
                    guard let url = URL(string: L10n.linksSupport) else { return }
                    let svc = SFSafariViewController(url: url)
                    self.present(svc, animated: true, completion: nil)
                } else {
                    let viewController = HomePageController(reactor: reactor.pageReactor(name: L10n.linksSupport))
                    let navigationController = UINavigationController(rootViewController: viewController)
                    self.present(navigationController, animated: true, completion: nil)
                }
            })
            .disposed(by: disposeBag)
        self.buttonMore.rx.tap
            .subscribe(onNext: { _ in
                let viewController = UserMoreController(reactor: reactor.moreReactor())
                let navigationController = UINavigationController(rootViewController: viewController)
                self.present(navigationController, animated: true, completion: nil)
            })
            .disposed(by: disposeBag)

        //social networks
        self.buttonInstagram.rx.tap
            .subscribe(onNext: { _ in
                guard let appURL = URL(string: "instagram://user?username=\(config["app"]["links"]["instagram"].string ?? "")") else { return }
                guard let webURL = URL(string: "https://instagram.com/\(config["app"]["links"]["instagram"].string ?? "")") else { return }
                if UIApplication.shared.canOpenURL(appURL) {
                    if #available(iOS 10.0, *) {
                        UIApplication.shared.open(appURL, options: [:], completionHandler: nil)
                    } else {
                        UIApplication.shared.openURL(appURL)
                    }
                } else {
                    if #available(iOS 10.0, *) {
                        UIApplication.shared.open(webURL, options: [:], completionHandler: nil)
                    } else {
                        UIApplication.shared.openURL(webURL)
                    }
                }
            })
            .disposed(by: disposeBag)
        self.buttonTwitter.rx.tap
            .subscribe(onNext: { _ in
                guard let appURL = URL(string: "twitter://user?screen_name=\(config["app"]["links"]["twitter"].string ?? "")") else { return }
                guard let webURL = URL(string: "https://twitter.com/\(config["app"]["links"]["twitter"].string ?? "")") else { return }
                if UIApplication.shared.canOpenURL(appURL) {
                    if #available(iOS 10.0, *) {
                        UIApplication.shared.open(appURL, options: [:], completionHandler: nil)
                    } else {
                        UIApplication.shared.openURL(appURL)
                    }
                } else {
                    if #available(iOS 10.0, *) {
                        UIApplication.shared.open(webURL, options: [:], completionHandler: nil)
                    } else {
                        UIApplication.shared.openURL(webURL)
                    }
                }
            })
            .disposed(by: disposeBag)
        self.buttonLinkedin.rx.tap
            .subscribe(onNext: { _ in
                guard let appURL = URL(string: "linkedin://company/\(config["app"]["links"]["linkedin"].string ?? "")") else { return }
                guard let webURL = URL(string: "https://linkedin.com/company/\(config["app"]["links"]["linkedin"].string ?? "")") else { return }
                if UIApplication.shared.canOpenURL(appURL) {
                    if #available(iOS 10.0, *) {
                        UIApplication.shared.open(appURL, options: [:], completionHandler: nil)
                    } else {
                        UIApplication.shared.openURL(appURL)
                    }
                } else {
                    if #available(iOS 10.0, *) {
                        UIApplication.shared.open(webURL, options: [:], completionHandler: nil)
                    } else {
                        UIApplication.shared.openURL(webURL)
                    }
                }
            })
            .disposed(by: disposeBag)
        self.buttonFacebook.rx.tap
            .subscribe(onNext: { _ in
                guard let appURL = URL(string: "fb://profile/\(config["app"]["links"]["facebook"].string ?? "")") else { return }
                guard let webURL = URL(string: "https://www.facebook.com/\(config["app"]["links"]["facebook"].string ?? "")") else { return }
                if UIApplication.shared.canOpenURL(appURL) {
                    if #available(iOS 10.0, *) {
                        UIApplication.shared.open(appURL, options: [:], completionHandler: nil)
                    } else {
                        UIApplication.shared.openURL(appURL)
                    }
                } else {
                    if #available(iOS 10.0, *) {
                        UIApplication.shared.open(webURL, options: [:], completionHandler: nil)
                    } else {
                        UIApplication.shared.openURL(webURL)
                    }
                }
            })
            .disposed(by: disposeBag)

        // contact
        self.buttonContact.rx.tap
            .subscribe(onNext: { _ in
                if MFMailComposeViewController.canSendMail() {
                    let mvc = MFMailComposeViewController()
                    mvc.mailComposeDelegate = self
                    mvc.setToRecipients([(config["app"]["mails"]["contact"].string ?? "")])
                    mvc.setSubject(L10n.userContact)
                    self.present(mvc, animated: true)
                }
            })
            .disposed(by: disposeBag)
        self.buttonReport.rx.tap
            .subscribe(onNext: { _ in
                if MFMailComposeViewController.canSendMail() {
                    let mvc = MFMailComposeViewController()
                    mvc.mailComposeDelegate = self
                    mvc.setToRecipients([(config["app"]["mails"]["report"].string ?? "")])
                    mvc.setSubject(L10n.userReport)
                    self.present(mvc, animated: true)
                }
            })
            .disposed(by: disposeBag)

        // error
        self.error.button?.rx.tap
            .subscribe(onNext: { _ in
                if MFMailComposeViewController.canSendMail() {
                    let mvc = MFMailComposeViewController()
                    mvc.mailComposeDelegate = self
                    mvc.setToRecipients([(config["app"]["mails"]["report"].string ?? "")])
                    mvc.setSubject(L10n.userReport)
                    mvc.setMessageBody(setMailError("\(reactor.currentState.error?.title ?? "") \n \(reactor.currentState.error?.description ?? "") \n  \(reactor.currentState.error?.source ?? "")"), isHTML: true)
                    self.present(mvc, animated: true)
                }
            })
            .disposed(by: disposeBag)
    }

    // MARK: actions (View -> Reactor)

    func bindAction(_ reactor: UserReactor) {
        // init
        self.rx.viewWillAppear
            .throttle(.milliseconds(Metric.timesRefreshData), latest: false, scheduler: MainScheduler.instance)
            .map { _ in Reactor.Action.get }
            .bind(to: reactor.action)
            .disposed(by: self.disposeBag)
        // refresh
        self.refreshControl.rx.controlEvent(.valueChanged)
            .map { Reactor.Action.get }
            .bind(to: reactor.action)
            .disposed(by: self.disposeBag)
        // buttons
        self.buttonInfo.rx.tap
            .subscribe(onNext: { _ in
                if let buttonTitle = self.buttonInfo.title(for: .normal), let role = reactor.currentState.user.roles?.last {
                    if buttonTitle == reactor.currentState.user.email {
                        self.buttonInfo.setTitle(role, for: .normal)
                    } else {
                        self.buttonInfo.setTitle(reactor.currentState.user.email, for: .normal)
                    }
                }

            })
            .disposed(by: self.disposeBag)
        self.buttonData.rx.tap
            .throttle(.milliseconds(Metric.timesButtonsThrottle), latest: false, scheduler: MainScheduler.instance)
            .subscribe(onNext: { _ in
                let actions: [AlertAction] = [AlertAction.action(title: L10n.modalConfirmationCancel, style: .cancel), AlertAction.action(title: L10n.modalConfirmationOk, style: .default)]
                self.showAlert(title: L10n.userData, message: L10n.userModalConfirmationDataMessage, style: .alert, actions: actions)
                    .filter { $0 == 1 }
                    .map { _ in Reactor.Action.data }
                    .bind(to: reactor.action)
                    .disposed(by: self.disposeBag)
            })
            .disposed(by: self.disposeBag)
        self.buttonLogout.rx.tap
            .throttle(.milliseconds(Metric.timesButtonsThrottle), latest: false, scheduler: MainScheduler.instance)
            .subscribe(onNext: { _ in
                let actions: [AlertAction] = [AlertAction.action(title: L10n.modalConfirmationCancel, style: .cancel), AlertAction.action(title: L10n.modalConfirmationOk, style: .destructive)]
                self.showAlert(title: L10n.userLogout, message: L10n.modalConfirmationMessage, style: .alert, actions: actions)
                    .filter { $0 == 1 }
                    .map { _ in Reactor.Action.logout }
                    .bind(to: reactor.action)
                    .disposed(by: self.disposeBag)
            })
            .disposed(by: self.disposeBag)
        self.buttonDelete.rx.tap
            .throttle(.milliseconds(Metric.timesButtonsThrottle), latest: false, scheduler: MainScheduler.instance)
            .subscribe(onNext: { _ in
                let actions: [AlertAction] = [AlertAction.action(title: L10n.modalConfirmationCancel, style: .cancel), AlertAction.action(title: L10n.modalConfirmationOk, style: .destructive)]
                self.showAlert(title: L10n.userDelete, message: L10n.userModalConfirmationDeleteMessage, style: .alert, actions: actions)
                    .filter { $0 == 1 }
                    .map { _ in Reactor.Action.delete }
                    .bind(to: reactor.action)
                    .disposed(by: self.disposeBag)
            })
            .disposed(by: self.disposeBag)
    }

    // MARK: states (Reactor -> View)

    func bindState(_ reactor: UserReactor) {
        // to update button content in eureka -> self.buttonProfil.updateCell()
        // image
        reactor.state
            .map { $0.user.email }
            .distinctUntilChanged()
            .subscribe(onNext: { email in
                if (reactor.currentState.user.avatar == "") {
                    self.imageAvatar.setImage(url: "https://secure.gravatar.com/avatar/\(email.md5)?s=200&d=mp")
                }
            })
            .disposed(by: self.disposeBag)
        reactor.state
            .map { $0.user.avatar }
            .distinctUntilChanged()
            .subscribe(onNext: { avatar in
                if (avatar != "") {
                    self.imageAvatar.setImage(url: setUploadImageUrl(avatar, sizes: [256, 512]), options: [.requestModifier(cookieModifier)])
                } else {
                    self.imageAvatar.setImage(url: "https://secure.gravatar.com/avatar/\(reactor.currentState.user.email.md5)?s=200&d=mp")
                }
            })
            .disposed(by: self.disposeBag)
        // social networks
        reactor.state
            .map { $0.configuration }
            .subscribe(onNext: { configuration in
                if(configuration["app"]["links"]["instagram"].string == "") {
                    self.buttonInstagram.hidden = true
                    self.buttonInstagram.evaluateHidden()
                }
            })
            .disposed(by: self.disposeBag)
        reactor.state
            .map { $0.configuration }
            .subscribe(onNext: { configuration in
                if(configuration["app"]["links"]["twitter"].string == "") {
                    self.buttonTwitter.hidden = true
                    self.buttonTwitter.evaluateHidden()
                }
            })
            .disposed(by: self.disposeBag)
        reactor.state
            .map { $0.configuration }
            .subscribe(onNext: { configuration in
                if(configuration["app"]["links"]["linkedin"].string == "") {
                    self.buttonLinkedin.hidden = true
                    self.buttonLinkedin.evaluateHidden()
                }
            })
            .disposed(by: self.disposeBag)
        reactor.state
            .map { $0.configuration }
            .subscribe(onNext: { configuration in
                if(configuration["app"]["links"]["facebook"].string == "") {
                    self.buttonFacebook.hidden = true
                    self.buttonFacebook.evaluateHidden()
                }
            })
            .disposed(by: self.disposeBag)
        // labels
        reactor.state
            .map { $0.user.firstName }
            .distinctUntilChanged()
            .subscribe(onNext: { firstName in
                self.labelName.text = "\(firstName) \(reactor.currentState.user.lastName)"
            })
            .disposed(by: self.disposeBag)
        reactor.state
            .map { $0.user.lastName }
            .distinctUntilChanged()
            .subscribe(onNext: { lastName in
                self.labelName.text = "\(reactor.currentState.user.firstName) \(lastName)"
            })
            .disposed(by: self.disposeBag)
        reactor.state
            .map { $0.user.email }
            .distinctUntilChanged()
            .subscribe(onNext: { email in
                self.buttonInfo.setTitle(email, for: .normal)
            })
            .disposed(by: self.disposeBag)
        reactor.state
            .map { $0.user.roles }
            .distinctUntilChanged()
            .subscribe(onNext: { roles in
                self.buttonInfo.setTitle(roles?.last, for: .normal)
            })
            .disposed(by: self.disposeBag)
        // refreshing
        reactor.state
            .map { $0.isRefreshing }
            .distinctUntilChanged()
            .bind(to: refreshControl.rx.isRefreshing)
            .disposed(by: disposeBag)
        // error
        reactor.state
            .map { $0.error }
            .filterNil()
            .throttle(.milliseconds(Metric.timesButtonsThrottle), latest: false, scheduler: MainScheduler.instance)
            .subscribe(onNext: { error in
                self.error.configureContent(title: error.title, body: error.description == "" ? error.title : error.description)
                self.error.button?.isHidden = (error.source != nil && error.code != 401) ? false : true
                SwiftMessages.hideAll()
                SwiftMessages.show(config: self.popupConfig, view: self.error)
            })
            .disposed(by: self.disposeBag)
    }
}