From 69fec5d3232a4ccf55336226443cd7d810286b53 Mon Sep 17 00:00:00 2001 From: Macostik Date: Tue, 21 Jan 2020 18:49:13 +0200 Subject: [PATCH] add NewsService --- FisherMan.xcodeproj/project.pbxproj | 20 ++ FisherMan/CommonClasses/Constants.swift | 10 +- FisherMan/CommonClasses/LanguageManager.swift | 69 ++++++ .../Coordinators/TabBarSceneCoordinator.swift | 1 + FisherMan/Extensions/String+Ext.swift | 214 ++++++++++++++++++ FisherMan/Models/LanguageModel.swift | 20 ++ FisherMan/Resources/Info.plist | 8 +- FisherMan/Services/NewsService.swift | 40 ++++ FisherMan/Services/RealmService.swift | 33 +++ FisherMan/Views/BaseViewController.swift | 13 -- FisherMan/Views/NewsSceneViewController.swift | 2 +- .../Views/ProfileSceneViewController.swift | 2 +- 12 files changed, 412 insertions(+), 20 deletions(-) create mode 100644 FisherMan/CommonClasses/LanguageManager.swift create mode 100644 FisherMan/Extensions/String+Ext.swift create mode 100644 FisherMan/Models/LanguageModel.swift create mode 100644 FisherMan/Services/NewsService.swift create mode 100644 FisherMan/Services/RealmService.swift diff --git a/FisherMan.xcodeproj/project.pbxproj b/FisherMan.xcodeproj/project.pbxproj index 2443c6e..6ba8d24 100644 --- a/FisherMan.xcodeproj/project.pbxproj +++ b/FisherMan.xcodeproj/project.pbxproj @@ -46,6 +46,11 @@ 56762AF423872025000E8501 /* SlimLoggerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56762AE223872025000E8501 /* SlimLoggerConfig.swift */; }; 56762AF9238AF51E000E8501 /* NSLayoutConstraint+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56762AF8238AF51E000E8501 /* NSLayoutConstraint+Ext.swift */; }; 56762AFB238AF5B3000E8501 /* UIView+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56762AFA238AF5B3000E8501 /* UIView+Ext.swift */; }; + 568659F123D6F92000FBB61C /* RealmService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568659F023D6F92000FBB61C /* RealmService.swift */; }; + 568659F323D75EED00FBB61C /* NewsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568659F223D75EEC00FBB61C /* NewsService.swift */; }; + 568659F523D75F8400FBB61C /* LanguageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568659F423D75F8400FBB61C /* LanguageManager.swift */; }; + 568659F723D75FBA00FBB61C /* LanguageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568659F623D75FBA00FBB61C /* LanguageModel.swift */; }; + 568659F923D761E200FBB61C /* String+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 568659F823D761E200FBB61C /* String+Ext.swift */; }; 56B9F7E423D59BD200C03F5E /* DetailSceneCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B9F7E023D59BD200C03F5E /* DetailSceneCoordinator.swift */; }; 56B9F7E523D59BD200C03F5E /* DetailSceneViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B9F7E123D59BD200C03F5E /* DetailSceneViewController.swift */; }; 56B9F7E623D59BD200C03F5E /* DetailSceneModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B9F7E223D59BD200C03F5E /* DetailSceneModel.swift */; }; @@ -113,6 +118,11 @@ 56762AE223872025000E8501 /* SlimLoggerConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlimLoggerConfig.swift; sourceTree = ""; }; 56762AF8238AF51E000E8501 /* NSLayoutConstraint+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Ext.swift"; sourceTree = ""; }; 56762AFA238AF5B3000E8501 /* UIView+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Ext.swift"; sourceTree = ""; }; + 568659F023D6F92000FBB61C /* RealmService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmService.swift; sourceTree = ""; }; + 568659F223D75EEC00FBB61C /* NewsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsService.swift; sourceTree = ""; }; + 568659F423D75F8400FBB61C /* LanguageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageManager.swift; sourceTree = ""; }; + 568659F623D75FBA00FBB61C /* LanguageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageModel.swift; sourceTree = ""; }; + 568659F823D761E200FBB61C /* String+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Ext.swift"; sourceTree = ""; }; 56B9F7E023D59BD200C03F5E /* DetailSceneCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailSceneCoordinator.swift; sourceTree = ""; }; 56B9F7E123D59BD200C03F5E /* DetailSceneViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailSceneViewController.swift; sourceTree = ""; }; 56B9F7E223D59BD200C03F5E /* DetailSceneModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailSceneModel.swift; sourceTree = ""; }; @@ -156,6 +166,8 @@ children = ( 4363693823CC710D00A620F6 /* InteractionAnimation.swift */, 436160B823CF2CCB0045AB81 /* InteractionNavigationController.swift */, + 568659F023D6F92000FBB61C /* RealmService.swift */, + 568659F223D75EEC00FBB61C /* NewsService.swift */, ); path = Services; sourceTree = ""; @@ -204,6 +216,7 @@ 56762ACD23872024000E8501 /* Environment.swift */, 56762ACE23872024000E8501 /* ApiManager.swift */, 56762ACF23872024000E8501 /* Dependency.swift */, + 568659F423D75F8400FBB61C /* LanguageManager.swift */, ); path = CommonClasses; sourceTree = ""; @@ -257,6 +270,7 @@ 436A8C5123C5DF5D00F0B44D /* NewsSceneModel.swift */, 56B9F7EA23D59D6600C03F5E /* SearchSceneModel.swift */, 43562C5E23C8CDAE00996D84 /* TabBarSceneModel.swift */, + 568659F623D75FBA00FBB61C /* LanguageModel.swift */, ); path = Models; sourceTree = ""; @@ -296,6 +310,7 @@ children = ( 56762AF8238AF51E000E8501 /* NSLayoutConstraint+Ext.swift */, 56762AFA238AF5B3000E8501 /* UIView+Ext.swift */, + 568659F823D761E200FBB61C /* String+Ext.swift */, ); path = Extensions; sourceTree = ""; @@ -498,13 +513,18 @@ 4363693723CC666000A620F6 /* CameraSceneCoordinator.swift in Sources */, 56762AF223872025000E8501 /* SlimLogglyDestination.swift in Sources */, 436160B923CF2CCB0045AB81 /* InteractionNavigationController.swift in Sources */, + 568659F523D75F8400FBB61C /* LanguageManager.swift in Sources */, 56762AE923872025000E8501 /* SplashSceneViewModel.swift in Sources */, + 568659F323D75EED00FBB61C /* NewsService.swift in Sources */, 56762AFB238AF5B3000E8501 /* UIView+Ext.swift in Sources */, 56B9F7EF23D59D6600C03F5E /* SearchSceneViewModel.swift in Sources */, 56762AF9238AF51E000E8501 /* NSLayoutConstraint+Ext.swift in Sources */, 56B9F80723D5A21300C03F5E /* ProfileSceneViewModel.swift in Sources */, + 568659F923D761E200FBB61C /* String+Ext.swift in Sources */, 43F99C4823A3D4DD008F6CD4 /* MainSceneViewController.swift in Sources */, + 568659F123D6F92000FBB61C /* RealmService.swift in Sources */, 43F99C5123A3DFA6008F6CD4 /* NewsSceneViewModel.swift in Sources */, + 568659F723D75FBA00FBB61C /* LanguageModel.swift in Sources */, 56B9F7E723D59BD200C03F5E /* DetailSceneViewModel.swift in Sources */, 4363693923CC710D00A620F6 /* InteractionAnimation.swift in Sources */, 56762AEA23872025000E8501 /* AppCoordinator.swift in Sources */, diff --git a/FisherMan/CommonClasses/Constants.swift b/FisherMan/CommonClasses/Constants.swift index c80c1aa..73a69f3 100644 --- a/FisherMan/CommonClasses/Constants.swift +++ b/FisherMan/CommonClasses/Constants.swift @@ -9,6 +9,12 @@ import UIKit import Foundation +//height controls +let keyWindow = UIApplication.shared.connectedScenes.filter({$0.activationState == .foregroundActive}) + .map({$0 as? UIWindowScene}).compactMap({$0}).first?.windows.filter({$0.isKeyWindow}).first + let navigationBarHeight = 44 + (keyWindow?.safeAreaInsets.bottom ?? 0) + let tabBarHeight = 44 + (keyWindow?.safeAreaInsets.bottom ?? 0) + struct Constants { static let baseURL = Environment.isProduction ? "http://nps-api-proxy.onespace.prod/api/v1/mobile/news" : Environment.isDevelop ? "http://nps-api-proxy.onespace.devel/api/v1/mobile/news" : @@ -18,5 +24,7 @@ struct Constants { static let iOS13Version = ProcessInfo.processInfo.operatingSystemVersion.majorVersion >= 13 static let screenWidth = UIScreen.main.bounds.width static let screenHeight = UIScreen.main.bounds.height - static let mainCollectionViewCell = "mainCollectionViewCell" + static let mainCollectionViewCell = "mainCollectionViewCell" + static let serviceID = "1fa7ce2d8fde588ac8fc" + static let localizeNames = "localizationsShortNames" } diff --git a/FisherMan/CommonClasses/LanguageManager.swift b/FisherMan/CommonClasses/LanguageManager.swift new file mode 100644 index 0000000..38ece14 --- /dev/null +++ b/FisherMan/CommonClasses/LanguageManager.swift @@ -0,0 +1,69 @@ +// +// LanguageManager.swift +// FisherMan +// +// Created by Yura Granchenko on 21.01.2020. +// Copyright © 2020 GYS. All rights reserved. +// + +import UIKit +import RxSwift +import RealmSwift + +enum Localable: String { + case en, ru +} + +final class LanguageManager { + + static let shared = LanguageManager() + fileprivate let disposeBag = DisposeBag() + public let notifyObservable = BehaviorSubject(value: ()) + + init() { + do { + let realm = try Realm() + guard let language = realm.objects(LanguageModel.self).first else { + let deviceLocale = Locale.preferredLanguages[0].prefix(2).toString() + let language = LanguageModel() + language.locale = deviceLocale + locale = Localable(rawValue: deviceLocale) ?? .en + try realm.write { + realm.add(language, update: .modified) + } + return + } + locale = Localable(rawValue: language.locale) ?? .en + } catch {} + } + + public var bundle: Bundle { + return Bundle(path: Bundle.main.path(forResource: locale.rawValue, ofType: "lproj") ?? "") + ?? Bundle.main + } + + public var locale: Localable = .en { + didSet { + Logger.verbose("Set new locale: \(locale.rawValue)") + do { + let realm = try Realm() + let language = LanguageModel() + language.locale = locale.rawValue + try realm.write { + realm.add(language, update: .modified) + } + notifyObservable.onNext(()) + } catch {} + } + } + + public var language: String { + let language = locale == .en ? "English" : "Russian" + return language + } + + public var isRussian: Bool { + return locale == .ru + } +} + diff --git a/FisherMan/Coordinators/TabBarSceneCoordinator.swift b/FisherMan/Coordinators/TabBarSceneCoordinator.swift index 0a2f4d8..44b3635 100644 --- a/FisherMan/Coordinators/TabBarSceneCoordinator.swift +++ b/FisherMan/Coordinators/TabBarSceneCoordinator.swift @@ -31,6 +31,7 @@ extension TabBarSceneCoordinator { } extension TabBarSceneModel { + func coordinator(window: UIWindow, dependencies: Dependency) -> BaseCoordinator { switch self { case .news: diff --git a/FisherMan/Extensions/String+Ext.swift b/FisherMan/Extensions/String+Ext.swift new file mode 100644 index 0000000..2cb0f48 --- /dev/null +++ b/FisherMan/Extensions/String+Ext.swift @@ -0,0 +1,214 @@ +// +// String+Ext.swift +// FisherMan +// +// Created by Yura Granchenko on 21.01.2020. +// Copyright © 2020 GYS. All rights reserved. +// + +import Foundation +import UIKit + +extension String { + + public var URL: Foundation.URL? { + return Foundation.URL(string: self as String) + } + + public var fileURL: Foundation.URL? { + return Foundation.URL(fileURLWithPath: self as String) + } + + public var smartURL: Foundation.URL? { + if isExistingFilePath { + return fileURL + } else { + return URL + } + } + + public var isExistingFilePath: Bool { + if hasPrefix("http") { + return false + } + return FileManager.default.fileExists(atPath: self as String) + } + + public var trim: String { + return trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } + + fileprivate static let emailRegex = "(?:[a-z0-9!#$%\\&'*+/=?\\^_`{|}~-]+(?:\\.[a-z0-9!#$%\\&" + + "'*+/=?\\^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|" + + "\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9]" + + "(?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4]" + + "[0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-" + + "\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])" + + public var isValidEmail: Bool { + guard let predicate = try? NSRegularExpression(pattern: String.emailRegex, + options: []) else { return false } + return predicate.firstMatch(in: self, + options: [], + range: NSMakeRange(0, count - 1)) != nil + } + + public var isValidPhone: Bool { + let phoneNumber = NSTextCheckingResult.CheckingType.phoneNumber.rawValue + guard let detector = try? NSDataDetector(types: phoneNumber) else { return false } + + if let match = detector.matches(in: self as String, + options: [], + range: NSMakeRange(0, (self as String).count)).first?.phoneNumber { + return match == self as String + } else { + return false + } + } + + public var URLQuery: [String: String] { + + var parameters = [String: String]() + for pair in components(separatedBy: "&") { + let components = pair.components(separatedBy: "=") + if components.count == 2 { + parameters[components[0]] = components[1].removingPercentEncoding + } else { + continue + } + } + return parameters + } + + private func clearPhoneNumber() -> String { + var phone = "" + for character in (self as String) { + if character == "+" || "0"..."9" ~= character { + phone.append(character) + } + } + return phone + } + + public var ls: String { + let string = self as String + return Bundle.main.localizedString(forKey: string, value: string, table: nil) + } + + private func randomString(length: Int) -> String { + let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + var result = "" + + for _ in 0.. CGFloat { + if isEmpty { return 0.0 } + let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) + let height = self.boundingRect(with: size, + options: .usesLineFragmentOrigin, + attributes: [.font: font], + context: nil).height + return ceil(height) + } + + public var localized: String { + return localized(for: LanguageManager.shared.bundle) + } + + public func localized(for bundle: Bundle) -> String { + return NSLocalizedString(self, bundle: bundle, comment: "") + } + + public var numbers: String { + return String(filter { "0"..."9" ~= $0 }) + } + + public func removeWhitespacesPattern() -> String { + return components(separatedBy: .whitespaces).joined() + } + + public func subString(by pattern: String) -> String? { + let regex = try? NSRegularExpression(pattern: pattern, options: []) + return regex?.matches(in: self, options: [], range: NSRange(location: 0, length: count)).map({ + self[Range($0.range, in: self)!].toString() + }).first + } + + public func removeSubstring(by pattern: String) -> String { + let regex = try? NSRegularExpression(pattern: pattern, options: []) + let output = regex?.stringByReplacingMatches(in: self, + options: [], + range: NSRange(location: 0, length: count), + withTemplate: "") + return output ?? "" + } + + public func applyPatternOnNumbers(pattern: String, replacmentCharacter: Character) -> String { + var pureNumber = self.replacingOccurrences( of: "[^0-9]", with: "", options: .regularExpression) + for index in 0 ..< pattern.count { + guard index < pureNumber.count else { return pureNumber } + let stringIndex = String.Index(encodedOffset: index) + let patternCharacter = pattern[stringIndex] + guard patternCharacter != replacmentCharacter else { continue } + pureNumber.insert(patternCharacter, at: stringIndex) + } + return pureNumber + } + + public func applyPattern() -> String { + return applyPatternOnNumbers(pattern: "## ### ####", + replacmentCharacter: "#") + } + + private func findIndexes(in string: String, isSearhing: Bool) -> [Int] { + var indexes = [Int]() + var searchStartIndex = startIndex + while startIndex < endIndex, let range = range(of: string, + options: .caseInsensitive, + range: searchStartIndex.. String { + return "\(self)" + } +} diff --git a/FisherMan/Models/LanguageModel.swift b/FisherMan/Models/LanguageModel.swift new file mode 100644 index 0000000..5de39d0 --- /dev/null +++ b/FisherMan/Models/LanguageModel.swift @@ -0,0 +1,20 @@ +// +// LanguageModel.swift +// FisherMan +// +// Created by Yura Granchenko on 21.01.2020. +// Copyright © 2020 GYS. All rights reserved. +// + +import Foundation +import RealmSwift + +final class LanguageModel: Object { + + @objc dynamic public var id = 0 + @objc dynamic public var locale = "en" + + override static func primaryKey() -> String? { + return "id" + } +} diff --git a/FisherMan/Resources/Info.plist b/FisherMan/Resources/Info.plist index b45c83b..6aa7db1 100644 --- a/FisherMan/Resources/Info.plist +++ b/FisherMan/Resources/Info.plist @@ -22,6 +22,10 @@ ${ENV} LSRequiresIPhoneOS + NSCameraUsageDescription + Accessing your camera + NSPhotoLibraryAddUsageDescription + Accessing photo library UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -49,9 +53,5 @@ UIInterfaceOrientationPortrait - NSCameraUsageDescription - Accessing your camera - NSPhotoLibraryAddUsageDescription - Accessing photo library diff --git a/FisherMan/Services/NewsService.swift b/FisherMan/Services/NewsService.swift new file mode 100644 index 0000000..b339e59 --- /dev/null +++ b/FisherMan/Services/NewsService.swift @@ -0,0 +1,40 @@ +// +// NewsService.swift +// FisherMan +// +// Created by Yura Granchenko on 21.01.2020. +// Copyright © 2020 GYS. All rights reserved. +// + +import Foundation +import RealmSwift + +class SplashService: RealmService { + + public func getAllNews(completion: (() -> Void)? = nil) { + var newsList = [NewsSceneModel]() + APIManager.allNews(["serviceId" : Constants.serviceID, + "localizationsShortNames": [Localable.ru.rawValue, Localable.en.rawValue]]) + .json().subscribe(onNext: { json in + do { + let realm = try Realm() + try realm.write { + let data = json["items"] + if !data.isEmpty { + for entity in data.arrayValue { + let object = realm.create(T.self, value: entity.object, update: .modified) + newsList.append(object) + } + Logger.verbose("Initiate news were loaded - \(data.count)") + } + } + completion?() + } catch let error { + Logger.error("DataBase of Realm was changed \(error)") + completion?() + } + }, onError: { _ in + completion?() + }).disposed(by: disposeBag) + } +} diff --git a/FisherMan/Services/RealmService.swift b/FisherMan/Services/RealmService.swift new file mode 100644 index 0000000..ad9b390 --- /dev/null +++ b/FisherMan/Services/RealmService.swift @@ -0,0 +1,33 @@ +// +// RealmService.swift +// FisherMan +// +// Created by Yura Granchenko on 21.01.2020. +// Copyright © 2020 GYS. All rights reserved. +// + +import Foundation +import RxSwift +import RxCocoa +import RxRealm +import RealmSwift + +protocol RealmServiceType { + associatedtype T + func observeEntries() -> Observable<([T], RxRealm.RealmChangeset?)>? +} + +public class RealmService: RealmServiceType { + typealias T = C + internal let disposeBag = DisposeBag() + + public func observeEntries() -> Observable<([T], RxRealm.RealmChangeset?)>? { + do { + let realm = try Realm() + let entries = realm.objects(T.self) + Logger.info("Entries type of \(type(of: T.self)) (\(entries.count) count) is available") + return Observable.arrayWithChangeset(from: entries) + } catch {} + return Observable.empty() + } +} diff --git a/FisherMan/Views/BaseViewController.swift b/FisherMan/Views/BaseViewController.swift index ca264f7..b225a4b 100644 --- a/FisherMan/Views/BaseViewController.swift +++ b/FisherMan/Views/BaseViewController.swift @@ -12,11 +12,6 @@ import RxSwift import RxCocoa import RealmSwift -let keyWindow = UIApplication.shared.connectedScenes.filter({$0.activationState == .foregroundActive}) - .map({$0 as? UIWindowScene}).compactMap({$0}).first?.windows.filter({$0.isKeyWindow}).first -let navigationBarHeight = 44 + (keyWindow?.safeAreaInsets.bottom ?? 0) -let tabBarHeight = 44 + (keyWindow?.safeAreaInsets.bottom ?? 0) - typealias ViewModelItem = BaseViewModel protocol ViewModelBased: class { @@ -75,14 +70,6 @@ class BaseViewController: UIViewController, ViewModelBased, BaseInstance { LastVisibleScreen.lastAppearedScreenName = screenName } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - } - override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } diff --git a/FisherMan/Views/NewsSceneViewController.swift b/FisherMan/Views/NewsSceneViewController.swift index 0a7dbd2..0dc5f6c 100644 --- a/FisherMan/Views/NewsSceneViewController.swift +++ b/FisherMan/Views/NewsSceneViewController.swift @@ -13,7 +13,7 @@ import RxCocoa class NewsSceneViewController: BaseViewController { override func setupUI() { - + view.backgroundColor = .blue } override func setupBindings() { diff --git a/FisherMan/Views/ProfileSceneViewController.swift b/FisherMan/Views/ProfileSceneViewController.swift index 8f1b4f4..76d975f 100644 --- a/FisherMan/Views/ProfileSceneViewController.swift +++ b/FisherMan/Views/ProfileSceneViewController.swift @@ -13,7 +13,7 @@ import RxCocoa class ProfileSceneViewController: BaseViewController { override func setupUI() { - + view.backgroundColor = .green } override func setupBindings() {