Placeholder views based on content, loading, error or empty states

Overview

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.

Comments
  • explicitly set Swift Version for tvOS

    explicitly set Swift Version for tvOS

    otherwise, the carthage build command for tvOS (/usr/bin/xcrun xcodebuild -project Example.xcodeproj -scheme StatefulViewController-tvOS -configuration Release -sdk appletvos ONLY_ACTIVE_ARCH=NO BITCODE_GENERATION_MODE=bitcode CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES clean build) fails

    opened by Lutzifer 6
  • UIActivityIndicatorView Always on top left

    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.

    opened by erickva 5
  • BUILD FAILED

    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?

    opened by russellyi 5
  • Add support for 'UITableViewController'

    Add support for 'UITableViewController'

    This works by creating a new ViewStateMachine subclass that manages a container view. The views for each state are added to this container view and this container view is added to the UITableViewController.view. When the state is set to .Content, the container view is removed from its superview, but retained by the state machine.

    No public APIs have been broken by this PR and the fix works automatically without any new code having to be implemented by the end user. Documentation for all new public classes, and properties has been provided. The demo project has been updated to use a UITableViewController to demonstrate this. It works exactly as expected in all scenarios.

    This PR closes #19.

    Please let me know if you have any questions about my implementation.

    opened by AnthonyMDev 5
  • Issue when the view controller's view is tableView/collectionView

    Issue when the view controller's view is tableView/collectionView

    Hi Awesome project, but we have an issue when the view of the UIViewController is an UITableview (example below)

    class VC: UIViewController {
        override func loadView() {
            view = UITableView()
        }
    }
    

    so it adds the loadingView/emptyView inside of the tableView

    opened by MarvinNazari 4
  • Add tvOS framework target (for tvOS carthage support)

    Add tvOS framework target (for tvOS carthage support)

    This PR Adds a tvOS framework target. The new consists of the exact same source files and even shares the Info.plist with the iOS target. I also added a new shared scheme so the tvOS target can be built using Carthage.

    Finally, I added a platform badge to the README.md to mark tvOS and iOS as supported platforms.

    Note: tvOS CocoaPods support is already handled in #16.

    opened by mathiasnagler 4
  • DispatchQueue.main.sync slightly delays showing initial loading view

    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() {
          ...
       }
    }
    
    opened by FWJonathan 3
  • crash with custom state

    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?

    opened by anneWe 3
  • Set a different frame in the loading View does not work

    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?

    opened by LucianoTurrini 3
  • currentState not updated after startLoading being called

    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)
                }
            }
        }
    
    opened by mschonvogel 3
  • Protocol usage

    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()
                                              }
    }
    
    opened by lfarah 2
  • UIView that conforms StatefulViewController protocol cannot be destroyed

    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
        }
    
    }
    
    opened by YK-Unit 2
  • Not available with Carthage and Xcode 10.2

    Not available with Carthage and Xcode 10.2

    error: SWIFT_VERSION '3.0' is unsupported, supported versions are: 4.0, 4.2, 5.0. (in target 'StatefulViewController-iOS') any plan to update to swift 4.x or 5.x?

    opened by emirandm 2
Releases(3.0.1)
Owner
Alexander Schuch
Swift developer. Classic Mini driver.
Alexander Schuch
DTTextField is a custom textfield with floating placeholder and error label

DTTextField Introduction DTTextField is a UITextField library with floating placeholder and error label. Floating placeholder inspired from JVFloatLab

Dhaval Thanki 310 Jan 5, 2023
TTextField is developed to help developers can initiate a fully standard textfield including title, placeholder and error message in fast and convinient way without having to write many lines of codes

TTextField is developed to help developers can initiate a fully standard textfield including title, placeholder and error message in fast and convinient way without having to write many lines of codes

Nguyen Duc Thinh 7 Aug 28, 2022
HTYTextField A UITextField with bouncy placeholder.

HTYTextField - A UITextField with bouncy placeholder. Screenshot Installation CocoaPods Add the dependency to your Podfile

Hanton Yang 312 Nov 13, 2022
Simple placeholder move textfield

PlaceholderTextField Example To run the example project, clone the repo, and run pod install from the Example directory first. Requirements iOS 11.0 o

null 6 Mar 9, 2022
A light-weight UITextView subclass that adds support for placeholder.

RSKPlaceholderTextView A light-weight UITextView subclass that adds support for placeholder. Installation Using Swift Package Manager To add the RSKPl

Ruslan Skorb 220 Dec 17, 2022
A UITextView subclass that adds support for multiline placeholder written in Swift.

KMPlaceholderTextView A UITextView subclass that adds support for multiline placeholder written in Swift. Usage You can set the value of the placehold

Zhouqi Mo 795 Nov 18, 2022
An UITextView in Swift. Support auto growing, placeholder and length limit.

GrowingTextView Requirements iOS 8.0 or above Installation CocoaPods GrowingTextView is available through CocoaPods. To install it, simply add the fol

Kenneth Tsang 941 Jan 5, 2023
DGPlaceholderTextView - A light-weight UITextView that supports for placeholder

DGPlaceholderTextView Requirements Installation Usage Properties DGPlaceholderTe

donggyu 5 Jan 26, 2022
UITextField-based control for (NS)Measurement values input.

MeasurementTextField UITextField-based control for (NS)Measurement values input. Provides type-safe keyboard and picker based input of different measu

Siarhei Fiedartsou 16 Jul 22, 2021
RichTextKit is a Swift-based library for working with rich text in UIKit, AppKit and SwiftUI.

About RichTextKit RichTextKit is a Swift-based library that lets you work with rich text in UIKit, AppKit and SwiftUI. RichTextKit is under developmen

Daniel Saidi 282 Dec 28, 2022
Placeholder views based on content, loading, error or empty states

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

Alexander Schuch 2.1k Dec 8, 2022
PJFDataSource is a small library that provides a simple, clean architecture for your app to manage its data sources while providing a consistent user interface for common content states (i.e. loading, loaded, empty, and error).

PJFDataSource PJFDataSource is a small library that provides a simple, clean architecture for your app to manage its data sources while providing a co

Square 88 Jun 30, 2022
A SwiftUI view for dynamically rendering content based upon "loading", "error", and "completed" data loading states.

SwiftUIAsyncContentView A SwiftUI view for dynamically rendering content based upon "loading", "error", and "completed" data loading states.. Installa

CypherPoet 0 Dec 26, 2021
Protocol to handle initial Loadings, Empty Views and Error Handling in a ViewController & views

StatusProvider Protocol to handle initial Loadings, Empty Views and Error Handling in a ViewController & views CocoaPods Podfile pod 'StatusProvider'

Mario Hahn 887 Dec 22, 2022
AsyncView is a SwiftUI View for handling in-progress and error states when loading data asynchronously.

AsyncView AsyncView is a SwiftUI View for handling in-progress and error states when loading data asynchronously using async/await: See my blog post "

Ralf Ebert 41 Dec 20, 2022
A UIControl subclass that makes it easy to create empty states.

AZEmptyState Making empty state simple. Screenshots Installation Cocoa Pods: pod 'AZEmptyState' Manual: Simply drag and drop the Sources folder to you

Antonio Zaitoun 88 Oct 2, 2022
Nice library to show placeholders and Empty States for any UITableView/UICollectionView in your project

HGPlaceholders Example To run the example project, clone the repo, and run pod install from the Example directory first. Requirements iOS 8.0+ Xcode 9

Hamza Ghazouani 2.2k Dec 24, 2022
A SwiftUI TextField with a prompt (or placeholder) that floats above the text field when active or not empty. Requires iOS 15.

FloatingPromptTextField A prompt is the label in a text field that informs the user about the kind of content the text field expects. In a default Tex

Emilio Peláez 43 Nov 3, 2022
Swiftui-pressed-states-example - Examples of Pressed States in SwiftUI

Examples of Pressed States in SwiftUI pressed-states.mp4

Philip Davis 6 Nov 15, 2022
ListPlaceholder is a swift library allows you to easily add facebook style animated loading placeholder to your tableviews or collection views.

ListPlaceholder ListPlaceholder Facebook news feed style animation Features ListPlaceholder is a swift library allows you to easily add facebook style

Moayad Al Kouz 628 Dec 19, 2022