UITabBarController Shared Data Model - share & update model from anywhere - ios

I'm using a TabBarcontroller type app and I'm using a shared model of the form:
enum WorkoutState {
case Stopped
case Started
case Paused
}
class BaseTBController: UITabBarController {
var workoutState: WorkoutState? = .Stopped
}
Currently all is working and I can access and update the variable across the different tabs using
let tabbar = tabBarController as! BaseTBController
if tabbar.workoutState = .Stop {
//do something
tabbar.workoutState = .Start
}
Now, the situation is that I seem to need to put this all over the place in my code. eg:
startRun()
resumeRun()
pauseRun()
Is there a better way to do this instead of putting
let tabbar = tabBarController as! BaseTBController
tabbar.workoutState = .Start
in each of the 3 functions?

You can always use protocol and default extension to achieve what you need
protocol HandleWorkStateProtocol where Self: UIViewController {
func updateWorkOutState(to: WorkoutState)
}
extension HandleWorkStateProtocol {
func updateWorkOutState(to state: WorkoutState) {
guard let tabBarController = self.tabBarController as? BaseTBController else { return }
tabBarController.workoutState = state
}
}
In all you view controller's that has these 3 methods (startRun, resumeRun, pauseRun) simply confirm to this protocol and call updateWorkOutState(to: with appropriate value to modify the status
class SomeTestViewController: UIViewController {
func startRun() {
self.updateWorkOutState(to: .Started)
}
func resumeRun() {
}
func pauseRun() {
self.updateWorkOutState(to: .Paused)
}
}
extension SomeTestViewController: HandleWorkStateProtocol {}
P.S
Case values of enum does not follow Pascal casing like Stopped instead it follows Camel casing stopped so change your enum values to
enum WorkoutState {
case stopped
case started
case paused
}

Related

add variable to all UIViewControllers

I'm new to Swift and I'm trying to implement a custom UIKeyCommand architecture in a practice app. I wrote the extension below for the base UISplitViewController to show all UIKeyCommands in the current views on screen.
extension UISplitViewController {
open override var canBecomeFirstResponder: Bool {
return true
}
var BPKeyCommands: [BPKeyCommand]? {
var commands: [BPKeyCommand] = []
var mastervc = self.viewControllers.first
if (mastervc is UINavigationController) {
mastervc = (mastervc as! UINavigationController).viewControllers.last
}
if let masterCommands = mastervc.commands {
for command in masterCommands {
commands.append(command)
}
}
return commands
}
open override var keyCommands: [UIKeyCommand]? {
var commands: [UIKeyCommand] = []
if let bpkeycommands = BPKeyCommands {
for command in bpkeycommands {
let new = UIKeyCommand(input: command.input,
modifierFlags: command.modifierFlags,
action: #selector(executeKeyCommand(sender:)),
discoverabilityTitle: command.title)
commands.append(new)
}
}
return commands
}
#objc private func executeKeyCommand(sender: UIKeyCommand) {
if let index = keyCommands?.firstIndex(of: sender) {
if let command = BPKeyCommands?[index] {
command.action(command)
}
}
}
}
Now, as you might expect this throws an error at if let masterCommands = mastervc.commands {, because UIViewController doesn't contain the commands variable out of the box. My question is: how can I haveUIViewControllerhave that variable? Just like all controllers can overridekeyCommands` by default?
You have to create a protocol with command variable and make your view controller conform to it (step 1). Either you can provide values for particular view controller or you can provide a default implementation.
step 1:- Create a protocol with the variable you need.
protocol Commandable{
var commands: [String]{set get}
}
extension Commandable{
var commands: [String]{
get{return ["hello","world"]}
set{}
}
}
step 2:- Make then controllers which you are using conform it
step 3:- change the above code to get commands
if let commandProtocol = masterVC as? Commandable
{
let commands = commandProtocol.commands
}
else{
// handle it
}
Make sure the variable is unique so you don't accidentally override it.
Thank you.
you can create a extension of UIViewController and add that property on that extension of UIViewController. Then You will get it on child view controllers like UISplitViewController or any other custom ViewControllers. To know more about extensions, Which can be added on extension or what can be done by extension??

Conditionally cast of generic view controller fails

Say I have the following:
class ContentSelectableViewController<T: NSManagedObject> : UIViewController { //... }
class PersonSelectionViewController: ContentSelectableViewController<Person> { // ... }
class PlaceSelectionViewController: ContentSelectableViewController<Place> { // ... }
Then in an instance of one of these subclasses, I have some code:
if let navCtrl = self.navigationController {
for viewController in navCtrl.viewControllers.reversed() {
if viewController is ContentSelectableViewController {
log.info("Worked for \(viewController.description)")
}
if let vc = viewController as? ContentSelectableViewController {
// This should be equivalent to the above.
}
}
}
My question is, when I have a stack full of subclasses of this generic baseclass, it doesn't always return true (go into the if statement) when checking if they are of type ContentSelectableViewController and I don't understand why. They inherit from the same baseclass.
EDIT:
I'm guessing it's because of the generic nature of the class. The if statements evaluate to true for the subclass that calls it.
So, it does in fact have something to do with trying to type check a generic class. It would work for the one and not the other because the one making the call implicitly adds its type.
i.e. (Pseudo-Swift)
if viewController is ContentSelectableViewController<Person> { //... }
What I did instead was to define a protocol that ultimately makes these ContentSelectableViewController<T> selectable:
enum ContentSelectionRole: Int {
case none = 0 // no selection going on right now.
case root // i.e. the one wanting content
case branch // an intermediary. think of a folder when looking for a file
case leaf // like a file
}
enum ContentSelectability: Int {
case noSelections = 0
case oneSelection = 1
case multipleSelections = 2
}
protocol ContentSelection {
var selectedObjects: [NSManagedObject] { get set }
var selectionRole: ContentSelectionRole { get set }
var selectionStyle: ContentSelectability { get set }
func popToSelectionRootViewController() -> Bool
func willNavigateBack(from viewController: UIViewController)
}
Making the definition:
class ContentSelectableViewController<T: NSManagedObject> : UIViewController, ContentSelection { //... }
And then, refactored the original post, to get:
#discardableResult func popToSelectionRootViewController() -> Bool {
if let navCtrl = self.navigationController {
for viewController in navCtrl.viewControllers.reversed() {
if let vc = viewController as? ContentSelection {
if vc.selectionRole == .root {
vc.willNavigateBack(from: self)
navCtrl.popToViewController(viewController, animated: true)
return true
}
}
}
}
return false
}
I still don't quite understand the aspect of the language that makes it fail, but this solution works.
Protocol-based Programming seems to be more Swifty anyway...

Swinject inject self's property into new UIViewController

Let's pretend we have an UITableViewController that on didSelectRowAtSection loads an instance of a class named i.e.: ClassToInject and it wants to inject it through a property injection because our ViewControllerToBePushed has a property of ClassToInject, that subsequently (because it's an UITabBarViewController) on the didSet callback it searches for all its viewControllers property that conforms to ClassToInjectPresentable simple as:
protocol ClassToInjectPresentable {
var property: ClassToInject { get set }
}
Until now, i would just do something like this:
func didSelectRowAtIndexPath {
let classToInject = self.loadClassToInjectFor(indexPath)
let tabBarViewController = SomeTabBarViewController()
tabBarViewController.property = classToInject
self.navigationController.push(tabBarViewController, animated: true)
}
And in SomeTabBarViewController ...
class SomeTabBarViewController: ClassToInjectPresentable {
var property: ClassToInject? {
didSet(newValue) {
self.viewControllers.filter{ $0 is ClassToInjectPresentable }.map{ $0 as! ClassToInjectPresentable }.forEach{ $0.property = newValue }
}
}
And everything should be get loaded nice and easy (but it's not). I've read about Swinject and this might be solved with it. I have seen lots of examples registering things like:
container.register(Animal.self) { _ in Cat(name: "Mimi") }
But I don't know if I can register some property that is loaded in self:
container.register(ClassToInjectInjector.self) { _ in
self.loadClassToInjectFor(indexPath) }
// And then
container.register(ClassToInjectPresentable.self) { _ in
SomeTabBarViewController() }
.initCompleted { r, p in
let tabBar = p as! SomeTabBarViewController
tabBar.property = r.resolve(ClassToInjectInjector.self)
// And lastly?
self.navigationController.pushViewController(tabBar, animated: true)
}
}
It is difficult to recommend proper solution without knowing details of your application, but here are some suggestions:
container.register(ClassToInjectInjector.self) { _ in
self.loadClassToInjectFor(indexPath)
}
In general, all register-ations should be done outside of your objects. Common setup ishaving one global Container, which contains all the registrations - you should look at them as instructions to build application objects without any implicit context. If your dependency needs to be created in the UITableViewController, you can pass it to resolve method as an argument:
container.register(ClassToInjectPresentable.self) { resolver, property in
let tabBar = SomeTabBarViewController()
tabBar.property = property
return tabBar
}
// in UItableVIewController
container.resolve(ClassToInjectPresentable.self,
argument: self.loadClassToInjectFor(indexPath))
Also this is usually a bad idea:
.initCompleted { r, p in
...
self.navigationController.pushViewController(tabBar, animated: true)
}
You should not mix application logic with DI - use Swinject purely for constructing your dependencies.
So your UITableViewController might look something like this:
func didSelectRowAtIndexPath {
let classToInject = self.loadClassToInjectFor(indexPath)
let tabBar = container.resolve(
SomeTabBarViewController.self, argument: loadClassToInjectFor(indexPath)
)
navigationController.push(tabBar, animated: true)
}
As for your TabBar and its view controllers: how do the UIViewControllers get into TabBar? Is it possible to do something like this?
class SomeTabBarViewController {
init(viewControllers: [UIViewController]) {
...
}
}
container.register(SomeTabBarViewController.self) { r, property
SomeTabBarViewController(viewControllers:[
r.resolve(MyViewController.self, argument: property),
r.resolve(MyViewController2.self, argument: property)
])
}
Finally I got the final answer by following the suggestions proposed.
public class Containers {
fileprivate init() { }
}
extension Containers {
static let activityPresentableContainer: Container = {
let container = Container()
container.register(ActivityTabBarController.self) { (r: Resolver, arg1: Activity) in
return ActivityTabBarController(activity: arg1)
}
container.register(ActivityPresentable.self) {
(r: Resolver, arg1: ActivityPresentableTabs, arg2: Activity) in
switch arg1 {
case .summary:
return ActivitySummaryViewController(activity: arg2)
case .detail:
return ActivityDetailPageViewController(activity: arg2)
case .map:
return ActivityMapViewController(activity: arg2)
case .charts:
return ActivityChartsViewController(activity: arg2)
case .strava:
return ActivityStravaViewController(activity: arg2)
}
}.inObjectScope(.transient)
return container
}()
With this approach, the named ActivityTabBarController gets instantiated always by the activityPresentableContainer using the following statement:
let controller = Containers.activityPresentableContainer.resolve(
ActivityTabBarController.self, argument: activity
)!
And then, each of the tabs inside the TabBarController gets instantiated using the required argument Activity and the type of tab itself using a .transient context. It resolves like this:
let activitySummary = Containers.activityPresentableContainer.resolve(
ActivityPresentable.self, arguments: ActivityPresentableTabs.summary, activity!
) as! UIViewController
This way I can generalize the tabs of the tab bar depending just on the information that they're using. If one of the tabs change in any moment, I can just change the registration, following the ActivityPresentable protocol.

How to show a view through protocol?

I want to detective networking state, when networking state changed, show a error view in current controller. But there is a problem by using protocol.
Here is the codes:
private func networkingDetection() {
//This is the detective method in appdelegate
try! reachability.startNotifier()
reachability.whenReachable = { [weak self] _ in
DispatchQueue.main.async {
self?.currentViewController().hideNetworkingErrorView()
}
}
reachability.whenUnreachable = { [weak self] _ in
DispatchQueue.main.async {
self?.currentViewController().showNetworkingErrorView()
}
}
}
And here is the protocol
protocol NetworkingErrorProtocol {
// I want to show the default view if there is no networkingErrorView, and
when declare a custom view in controller, show the custom view.
//var networkingErrorView: UIView? { get }
func showNetworkingErrorView()
func hideNetworkingErrorView()
}
extension UIViewController: NetworkingErrorProtocol {
func showNetworkingErrorView() {
}
func hideNetworkingErrorView() {
}
}
Anyone can tell me how to figure it out? It's really makes me crazy. Thanks a lot.
The issue with your setup is that conforming UIViewController to your protocol does not allow you to receive that call in your subclass. If you try to override the protocol function in your subclass you will get a compiler error: Declarations from extensions cannot be overridden yet
First off, a note about NotificationCenter. If you need multiple parts of your app to be notified of the change that would be a good way to go. If you only need to tell one controller, this is a classic usage for a delegate.
Here are two ways to get the desired functionality: using the delegate pattern and without.
Let's say Manager is the class where the monitoring is happening:
Using a delegate pattern
class Manager {
weak var networkDelegate : NetworkStatusListener?
func monitorNetworkStatus() {
var reachable = true;
if reachable {
// We can call the delegate directly
networkDelegate?.networkStatusChanged(.connected)
}
else {
networkDelegate?.networkStatusChanged(.disconnected)
}
}
}
And the same Manager without a delegate pattern. This would be the simplest fix for your current implementation issue.
class Manager {
func currentViewController() -> UIViewController { return vc }
func monitorNetworkStatus() {
var maybeAtListener = currentViewController()
// DON't SHIP THIS, but it can be helpful during development to make sure you didn't forget to conform one of your classes
assert(maybeAtListener is NetworkStatusListener, "Oops, did you mean to conform \(currentVC) to NetworkStatusListener")
var reachable = true;
if reachable {
// We can't be sure the controller conforms to the protocol but we can try
(maybeAtListener as? NetworkStatusListener)?.networkStatusChanged(.connected)
}
else {
(maybeAtListener as? NetworkStatusListener)?.networkStatusChanged(.connected)
}
}
}
Then for your view controller
class MyController : UIViewController, NetworkStatusDelegate {
func networkStatusChanged(_ status: NetworkStatus) {
switch status {
case .connected:
// Normal UI
break
case .disconnected:
// No network connect
break;
}
}
}
Also, not directly related to your question but for this example I used a slightly different approach to the protocol design that can be helpful for "status" oriented protocols. Having multiple functions can often become a little more tedious to conform to as protocols get larger.
enum NetworkStatus {
case connected
case disconnected
}
protocol NetworkStatusListener : class {
func networkStatusChanged(_ status: NetworkStatus)
}
Try using reachability class's NSNotificationCenter
add this in appdelegate's didFinishLaunchingWithOptions if you want for whole app
OR add in your specific viewcontroller if you want this in specific Viewcontroller
NotificationCenter.default.addObserver(self, selector:Selector(("checkForReachability:")), name: NSNotification.Name.reachabilityChanged, object: nil);
let reachability: Reachability = Reachability.forInternetConnection();
reachability.startNotifier();
This method called while network state changed .
func checkForReachability(notification:NSNotification)
{
let networkReachability = notification.object as! Reachability;
_ = networkReachability.currentReachabilityStatus()
// do yor additional work here
}

Prevent runtime crash for function instantiateViewController(withIdentifier:)

Swift 3.0
From what I found here is UIStoryboard always return non-optional instance in function instantiateViewController(withIdentifier:).
open class UIStoryboard : NSObject {
...
open func instantiateViewController(withIdentifier identifier: String) -> UIViewController
}
The crash happen if we adding wrong identifier value, without noticed.
This case might happen in complex projects which have large number of controller in many storyboards, and a controller StoryboardId is missed.
The only solution I found is making a function to temporary create all controllers when start app delegate (this function should be called in debugging mode only).
func validateControllers() {
guard let _ = xyzStoryboard.instantiateViewController(withIdentifier: "ABC") as? ABCViewController else {
fatalError("fail in init controller ABC from storyboard XYZ")
}
...
}
But I wonder if we could have another way to handle this situation. Or should I raise this issue to Swift team? Thanks!
as far as i know there is no solution for preventing crashes like this. but this is a good thing and it's meant to crash like this to show you there is something wrong with your code and you need to fix them!
Writing this answer to just to let you know how we handled the accidental typo(s) of a View Controller Identifier, which could lead to error when you try to create an ViewController from Storyboard(s) using view controller's identifier.
We had a complex project which had almost 15-20 ViewControllers, we didn't put them in a single storyboard instead we shared these VCs across multiple story boards and then we created an object called StoryBoardManager, which would create an VC from various storyboard(s) and hand it over to us.
We also created couple of enums to represent various storyboard(s) and viewController(s) inside.
It somewhat looks like this,
enum Storyboards : String {
case main = "Main"
case signup = "SignUp"
case discover = "Discover"
case utility = "Utility"
case event = "Event"
}
enum ViewControllers : String {
case login = "login"
case onBoard = "on_board"
case signup = "signup"
case signupNavigation = "signupNavigaitonVC"
case discoverNavigation = "discoverNavigation"
case individualProfileSetUp = "individualProfileSetUp"
case organizationProfileSetUp = "organizationProfileSetUp"
case discover = "discover"
case imagePickerMenuVC = "imagePickerMenuVC"
case eventDiscoverMapNavigation = "eventDiscoverMapNavigationVC"
case eventDiscoverMapVC = "eventDiscoverMapVC"
case event = "event"
}
class StoryboardManager {
static func storyboard(name:Storyboards) -> UIStoryboard {
let aStoryboard = UIStoryboard(name: name.rawValue, bundle: nil)
return aStoryboard
}
static func viewController(storyboardId:Storyboards, viewControllerId:ViewControllers) -> UIViewController {
let storyboard = StoryboardManager.storyboard(storyboardId)
let viewcontroller = storyboard.instantiateViewControllerWithIdentifier(viewControllerId.rawValue)
return viewcontroller
}
}
We mainly did this to avoid the typo mistakes for the ViewController identifier(s), which would lead to an runtime error. You will add all the viewController(s) identifiers to ViewControllers enum and storyboard(s) names to the Storyboards enum as a enum case. We updated both the enums whenever we introduced a new storyboard or a new ViewController in any of the storyboards and this helped all the team members not to make the typo for ViewController identifier.
Then we created ViewController(s) using the below code,
let loginVC = StoryboardManager.viewController(.main, viewControllerId: .login)
HTH :)
Short brief
Currently, I have a complex project that have 5 storyboards, with 20 controllers. In my case, using storyboard segue seems not a good idea. Therefore, creating controller's instance from storyboard and make a transition by code is much better.
But the problem is that instantiateViewController(withIdentifier:) always return non-optional instance, and it will be crashed if we put an invalid identifier (which didn't define in storyboard).
Solution (Swift 3.0)
There are steps to prevent this problem.
In *.storyboard, use controller class name as StoryboardID
Create UIStoryboard+Ext.swift to define all storyboards & controllers belong
Create BaseViewController.swift to define the initialization from storyboard
Create function to init all storyboards & controllers, to prevent crashing in runtime.
Download sample code from if you want to try yourself: https://github.com/nahung89/StoryboardPattern
Detail (TL;DR)
Step 1:
Step 2:
extension UIStoryboard {
enum Identifier: String {
case main = "Main"
case explore = "Explore"
case search = "Search"
case profile = "Profile"
}
static func name(for controller: UIViewController.Type) -> String? {
var identifier: Identifier?
switch controller.className {
case HomeViewController.className:
identifier = .main
case ExploreViewController.className:
identifier = .explore
case SearchViewController.className:
identifier = .search
case ProfileViewController.className:
identifier = .profile
default:
break
}
return identifier?.rawValue
}
}
extension UIStoryboard {
static func instanceFromIdentifier(_ identifier: Identifier) -> UIStoryboard {
return UIStoryboard(name: identifier.rawValue, bundle: Bundle.main)
}
static func instanceFromName(_ name: String) -> UIStoryboard? {
guard let identifier = Identifier(rawValue: name) else { return nil }
return instanceFromIdentifier(identifier)
}
}
Step 3:
extension UIViewController {
var className: String {
return String(describing: type(of: self))
}
class var className: String {
return String(describing: self)
}
}
class BaseViewController: UIViewController {
static var controllerId: String {
return String(describing: self) // return slass name, i.e "ExploreViewController"
}
static func instanceFromStoryboard() -> Self? {
guard let storyboardName = UIStoryboard.name(for: self) else { return nil }
return instantiateFrom(storyboardName: storyboardName)
}
private static func instantiateFrom<VC: UIViewController>(storyboardName: String) -> VC? {
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: controllerId) as? VC
return controller
}
}
Step 4
extension AppDelegate {
func validateStoryboards() {
guard
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.main.rawValue),
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.explore.rawValue),
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.profile.rawValue),
let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.search.rawValue)
else {
fatalError("fail to init storyboard by name")
}
guard let _ = HomeViewController.instanceFromStoryboard(),
let _ = ExploreViewController.instanceFromStoryboard(),
let _ = ProfileViewController.instanceFromStoryboard(),
let _ = SearchViewController.instanceFromStoryboard()
else {
fatalError("fail to init controller from storyboard")
}
}
}

Resources