Code Monkey home page Code Monkey logo

statefulviewcontroller's Introduction

StatefulViewController

Build Status Carthage compatible Swift 3.0 Platform

A protocol to enable UIViewControllers or UIViews to present placeholder views based on content, loading, error or empty states.

StatefulViewController Example

Overview

In a networked application a view controller or custom view typically has the following states that need to be communicated to the user:

  • Loading: The content is currently loaded over the network.
  • Content: The content is available and presented to the user.
  • Empty: There is currently no content available to display.
  • Error: An error occurred whilst downloading content.

As trivial as this flow may sound, there are a lot of cases that result in a rather large decision tree.

Decision Tree

StatefulViewController is a concrete implementation of this particular decision tree. (If you want to create your own modified version, you might be interested in the state machine that is used to show and hide views.)

Version Compatibility

Current Swift compatibility breakdown:

Swift Version Framework Version
3.0 3.x
2.3 2.x
2.2 1.x

Usage

This guide describes the use of the StatefulViewController protocol on UIViewController. However, you can also adopt the StatefulViewController protocol on any UIViewController subclass, such as UITableViewController or UICollectionViewController, as well as your custom UIView subclasses.

First, make sure your view controller adopts to the StatefulViewController protocol.

class MyViewController: UIViewController, StatefulViewController {
    // ...
}

Then, configure the loadingView, emptyView and errorView properties (provided by the StatefulViewController protocol) in viewDidLoad.

override func viewDidLoad() {
    super.viewDidLoad()

    // Setup placeholder views
    loadingView = // UIView
    emptyView = // UIView
    errorView = // UIView
}

In addition, call the setupInitialViewState() method in viewWillAppear: in order to setup the initial state of the controller.

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    setupInitialViewState()
}

After that, simply tell the view controller whenever content is loading and StatefulViewController will take care of showing and hiding the correct loading, error and empty view for you.

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    loadDeliciousWines()
}

func loadDeliciousWines() {
    startLoading()

    let url = NSURL(string: "http://example.com/api")
    let session = NSURLSession.sharedSession()
    session.dataTaskWithURL(url) { (let data, let response, let error) in
        endLoading(error: error)
    }.resume()
}

Life cycle

StatefulViewController calls the hasContent method to check if there is any content to display. If you do not override this method in your own class, StatefulViewController will always assume that there is content to display.

func hasContent() -> Bool {
    return datasourceArray.count > 0
}

Optionally, you might also be interested to respond to an error even if content is already shown. StatefulViewController will not show its errorView in this case, because there is already content that can be shown.

To e.g. show a custom alert or other unobtrusive error message, use handleErrorWhenContentAvailable: to manually present the error to the user.

func handleErrorWhenContentAvailable(error: ErrorType) {
    let alertController = UIAlertController(title: "Ooops", message: "Something went wrong.", preferredStyle: .Alert)
    alertController.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
    presentViewController(alertController, animated: true, completion: nil)
}

Custom Placeholder View insets

Per default, StatefulViewController presents all configured placeholder views fullscreen (i.e. with 0 insets from top, bottom, left & right from the superview). In case a placeholder view should have custom insets the configured placeholderview may conform to the StatefulPlaceholderView protocol and override the placeholderViewInsets method to return custom edge insets.

class MyPlaceholderView: UIView, StatefulPlaceholderView {
    func placeholderViewInsets() -> UIEdgeInsets {
        return UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
    }
}

View State Machine

Note: The following section is only intended for those, who want to create a stateful controller that differs from the flow described above.

You can also use the underlying view state machine to create a similar implementation for your custom flow of showing/hiding views.

let stateMachine = ViewStateMachine(view: view)

// Add states
stateMachine["loading"] = loadingView
stateMachine["other"] = otherView

// Transition to state
stateMachine.transitionToState(.View("loading"), animated: true) {
    println("finished switching to loading view")
}

// Hide all views
stateMachine.transitionToState(.None, animated: true) {
    println("all views hidden now")
}

Installation

Carthage

Add the following line to your Cartfile.

github "aschuch/StatefulViewController" ~> 3.0

Then run carthage update.

CocoaPods

Add the following line to your Podfile.

pod "StatefulViewController", "~> 3.0"

Then run pod install with CocoaPods 0.36 or newer.

Manually

Just drag and drop the two .swift files in the StatefulViewController folder into your project.

Tests

Open the Xcode project and press โŒ˜-U to run the tests.

Alternatively, all tests can be run from the terminal using xctool.

xctool -scheme StatefulViewControllerTests -sdk iphonesimulator test

Todo

  • Default loading, error, empty views
  • Protocol on views that notifies them of removal and add
  • Views can provide delays in order to tell the state machine to show/remove them only after a specific delay (e.g. for hide and show animations)

Contributing

  • Create something awesome, make the code better, add some functionality, whatever (this is the hardest part).
  • Fork it
  • Create new branch to make your changes
  • Commit all your changes to your branch
  • Submit a pull request

Contact

Feel free to get in touch.

statefulviewcontroller's People

Contributors

5d avatar aschuch avatar buttacciot avatar jamesruston avatar jobinsjohn avatar lutzifer avatar mathiasnagler avatar mattfinlayson avatar philippeauriach avatar readmecritic avatar yonaskolb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

statefulviewcontroller's Issues

SPM Support

Seems that the lib doesn't support Swift Package Manager. It would be great if it was supported.

failureView.tapGestureRecognizer not working in UITableViewController.

failureView.tapGestureRecognizer is not working in UITableViewController, Although working in UICollectionView.
.
So, please let me know the possible solution.

I have wrote following code:

loadingView = LoadingView(frame: self.tableView.frame)
        emptyView = EmptyView(frame: self.tableView.frame)
        let failureView = ErrorView(frame: self.tableView.frame)
        failureView.tapGestureRecognizer.addTarget(self, action: #selector(self.loadReadingData_api))
        errorView = failureView

tvOS

Hi

Would you be able to push a new release to cocoapods, so the tvOS changes in the podspec come through?

UIActivityIndicatorView Always on top left

UIActivityIndicatorView as loadingView Always on top left even when it's set with a larger frame, it is always small in the corner.
Is there a work around this? As the insets method applies for all the state views.

Loading state is not a initial state?

When I open my view controller I can see my empty view momentarily then it switches back to loading state. How to avoid that? Either you introduce a new "Initial State" or make Loading state by default state.

Outside contributors / maintaining

Hey @aschuch ๐Ÿ‘‹

This seems like a useful lib for people, but I worry that no one will help if it's not actively maintained. Are there two or three people you could give contributor access to โ€” preferably those who've contributed code โ€” to help keep it alive?

Otherwise, it's hard to have confidence in using this when there are still minor open issues from over a year ago.

Thoughts?

Not working with abstract controller class?

If we make the following changes to the TableViewController from examples:

class AbstractTableViewController: UITableViewController, StatefulViewController {
}

class TableViewController: AbstractTableViewController {

The view doesn't change its state at all when calling startLoading or any other method. Why could that be? I'm new to Swift/iOS development and might not be aware of something. It's just I'm used to create reusable abstract classes to make the code more DRY in other languages, would be a shame if I had to write same boilerplate code in all my controllers where I need LCE (Loading-Content-Error) states.

** CLEAN FAILED **** BUILD FAILED **

xcode 8.2

*** Building scheme "StatefulViewController-iOS" in Example.xcodeproj
** CLEAN FAILED **

The following build commands failed:
Check dependencies
(1 failure)
** BUILD FAILED **

The following build commands failed:
Check dependencies
(1 failure)
A shell task (/usr/bin/xcrun xcodebuild -project /projects/PangPingfei/SuperDemo/SuperDemo/Carthage/Checkouts/StatefulViewController/Example.xcodeproj -scheme StatefulViewController-iOS -configuration Release -sdk iphoneos ONLY_ACTIVE_ARCH=NO BITCODE_GENERATION_MODE=bitcode CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES clean build) failed with exit code 65:
** CLEAN FAILED **

The following build commands failed:
Check dependencies
(1 failure)
** BUILD FAILED **

The following build commands failed:
Check dependencies
(1 failure)

Feature: Support "Partial State"

@aschuch - Way to go and thanks for this really great library!

Small suggestion for the future:

Would be also great if the library could support the Partial State, as I learned from this great post of Scott Hurff:

"The partial state is the screen someone will see when the page is no longer empty and sparsely populated."

Wrong state when network initially disabled

I discovered an "issue" when using StatefulViewController. When you turn off wi-fi and make request, NetworkManager will back with failure block before viewWillAppearMethodwill be invoked. This makes for a short while errorView visible, then statefulMachine will change this view back to loading mode, what is untrue.

Here is code:
in viewDidLoad() I setup all states views:

emptyView = EmptyView(frame: view.frame)
errorView = ErrorView(frame: view.frame)
loadingView = LoadingView(frame: view.frame)
// and start loading data:
loadData()

loadData() method implementation:

func loadData() {

        if (lastState == .Loading) { return }

        startLoading()
        NetworkManager.sharedInstance.makeRequest({ (response) -> Void in
            /* do something with response*/
            self.endLoading()
        }, failure: { (error) -> () in
            self.endLoading(error: error)
        })
    }

Workaround:
Place loadData() method in viewWillAppear() after calling super. What force me to use self.isMovingToParentViewController() flag, when my controller hierarchy is expanded. Maybe you should consider to call:

// Make sure to stay in the correct state when transitioning
let isLoading = (currentState == .Loading)
let error: NSError? = (currentState == .Error) ? NSError() : nil
transitionViewStates(loading: isLoading, error: error, animated: false)

manually rather than clip it to viewWillAppear() ?

DispatchQueue.main.sync slightly delays showing initial loading view

If already on the main thread, the call to DispatchQueue.main.sync in transitionToState(...) will delay a loading view presentation briefly so that the main view is shown before valid data is present.

This could be resolved with something like the following logic:

if Thread.isMainThread {
      ...
} else {
   DispatchQueue.main.sync() {
      ...
   }
}

placeholder views aligned to top of UITableViewController

I set up my placeholder view as usual inside of a UITableViewController using the view's frame, but the placeholder views don't take the entire view's frame instead are aligned to the top with parts hiding under the navigation controller. How can I resolve this please?

Why minimum platform version is set to iOS8 ?

We plan on using your nice component on a project but we are compatible to iOS 7 and above.
Is there a complex issue in the current implementation preventing from using it on iOS7 ? Or is it just the cocoapod specs ?

Fix Travis CI build

Travis CI has an outdated version of Xcode 6.1 installed. The build fails because of a recent change in the swift compiler. ๐Ÿ˜ž

crash with custom state

Hi, thanks for this helpful library!
I'm currently struggeling to add a custom state. I tried to add the additional state to the state machine as described in the readme. Unfortunately, the app crashes when it tries to create the StatefulViewControllerState with my custom viewKey in the getter of currentState and lastState, because the initialization of the state is forced to not be nil: StatefulViewControllerState(rawValue: viewKey)! and my custom viewKey is not part of the default state enum.
How can this be solved?

Clean checkout fails to build

Checking this project out the master branch or 3.0 tag does not compile under Xcode 8.2.

The compiler complains about SWIFT_VERSION and setting the value to 3.0 fixes the issue (Beanhunter@3c9781e)

It looks like you have set the swift version to 3.0.1 intentionally, so I have raised this as an issue rather than a PR.

BUILD FAILED

carthage version: 0.16.2
xcodebuild -version: Xcode 8.0 Build version 8A218a

following problem does occur when i used carthage update

xcodebulid Output

A shell task (/usr/bin/xcrun xcodebuild -project /Users/donnieyi/Workspace/CampusAssistant/CampusAssistant-iOS/Carthage/Checkouts/StatefulViewController/Example.xcodeproj -scheme StatefulViewController-iOS -configuration Release -sdk iphoneos ONLY_ACTIVE_ARCH=NO BITCODE_GENERATION_MODE=bitcode CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES clean build) failed with exit code 65:

Someone had this problem?

currentState not updated after startLoading being called

The loadInitialData method is called multiple times (viewDidLoad, UIApplicationDidBecomeActive) and is always doing the Network request. After calling startLoading() the currentState var will not be updated. The printed loadingState is always content.

    func loadInitialData()
    {
        println(currentState.rawValue)
        if (currentState == .Loading) {
            return
        }
        startLoading()
        println("loadingState: \(currentState.rawValue)")
        FoyerApi.shared.getStories(refresh:true, lastStoryId: nil) { (stories:[Story]?, error:NSError?) in
            self.endLoading(error: error)
            if let _stories = stories {
                self.insertStories(_stories, refresh: true)
            }
        }
    }

Add instructions to change backing view.

I want a subview of my controller to change loading state. Is it possible?

I added this to my controller but doesn't work

var backingView: UIView {
    return contentView
}

Swift 3 pod version

It would be really great if we could get a Swift 3 version pushed to the CocoaPods master spec repo soon.

Thank you!

Not working on a UITableViewController

Hi, this is a great lib, I used it over a UIViewController and works wonderfull

But then I tried to use it on a UITableViewController and it does not work at all ๐Ÿ˜“

UIView that conforms StatefulViewController protocol cannot be destroyed

UIView that conforms StatefulViewController protocol does not be destroyed.

Here is a StatefulViewControllerDemo.zip to illustrate this issue.

PS: the core code

class StateView: UIView, StatefulViewController {

    public var defaultLoadingView: UIView? {
        let defaultView = UIView()
        defaultView.backgroundColor = UIColor.white

        let label = UILabel()
        label.textColor = UIColor.blue
        label.text = "loading..."
        defaultView.addSubview(label)
        label.snp.makeConstraints { (maker) in
            maker.height.equalTo(20)
            maker.centerX.equalToSuperview()
            maker.top.equalToSuperview().offset(160)
        }
        return defaultView
    }

    public var defaultEmptyView: UIView? {
        let defaultView = UIView()
        defaultView.backgroundColor = UIColor.white

        let label = UILabel()
        label.textColor = UIColor.blue
        label.text = "no content"
        defaultView.addSubview(label)
        label.snp.makeConstraints { (maker) in
            maker.height.equalTo(20)
            maker.centerX.equalToSuperview()
            maker.top.equalToSuperview().offset(160)
        }
        return defaultView
    }


    override init(frame: CGRect) {
        super.init(frame: frame)

        loadingView = defaultLoadingView
        emptyView = defaultEmptyView
        setupInitialViewState()
    }

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

    // FIXME: StateView cannot be destroyed
    deinit {
        print("StateView deinit ...")
    }

    func hasContent() -> Bool {
        return false
    }

}

Protocol usage

As Ray Wenderlich discusses here, when adding protocol conformance to a model, prefer adding a separate extension for the protocol methods..

So this:

class MyViewController: UIViewController, StatefulViewController {
    // ...
}

should be like this:

extension MyViewController: StatefulViewController {

func loadDeliciousWines() {
    startLoading()

    let url = NSURL(string: "http://example.com/api")
    let session = NSURLSession.sharedSession()
    session.dataTaskWithURL(url) { (let data, let response, let error) in
        endLoading(error: error)
    }.resume()
                                          }
}

Access NSError Object in ErrorView

In order to give the user a more concrete info than just "Something went wrong.", it would be straightforward to have access to the NSError object in the view.

Set a different frame in the loading View does not work

Thanks for this really great library!

I have a problem:

I can not put a different frame for LoadingView, for example:

loadingView = UIView (frame: CGRectMake (0, 0, 100, 100))

this does not work, the loadingView always frame the superview, never changes, can help me?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.