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
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
Advanced List View for SwiftUI with pagination & different states

AdvancedList This package provides a wrapper view around the SwiftUI List view which adds pagination (through my ListPagination package) and an empty,

Chris 246 Jan 3, 2023
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
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 794 Jan 7, 2023
A custom stretchable header view for UIScrollView or any its subclasses with UIActivityIndicatorView and iPhone X safe area support for content reloading. Built for iOS 10 and later.

Arale A custom stretchable header view for UIScrollView or any its subclasses with UIActivityIndicatorView support for reloading your content. Built f

Putra Z. 43 Feb 4, 2022
A SwiftUI Library for creating resizable partitions for View Content.

Partition Kit Recently Featured In Top 10 Trending Android and iOS Libraries in October and in 5 iOS libraries to enhance your app! What is PartitionK

Kieran Brown 230 Oct 27, 2022
A controller that uses a UIStackView and view controller composition to display content in a list

StackViewController Overview StackViewController is a Swift framework that simplifies the process of building forms and other static content using UIS

Seed 867 Dec 27, 2022
SwiftUI view enabling navigation between pages of content, imitating the behaviour of UIPageViewController for iOS and watchOS

PageView SwiftUI view enabling page-based navigation, imitating the behaviour of UIPageViewController in iOS. Why SwiftUI doesn't have any kind of pag

Kacper Rączy 365 Dec 29, 2022
A SwiftUI ScrollView that only scrolls if the content doesn't fit in the View

ScrollViewIfNeeded A SwiftUI ScrollView that only scrolls if the content doesn't fit in the View Installation Requirements iOS 13+ Swift Package Manag

Daniel Klöck 19 Dec 28, 2022
A nice iOS View Capture Swift Library which can capture all content.

SwViewCapture A nice iOS View Capture Library which can capture all content. SwViewCapture could convert all content of UIWebView to a UIImage. 一个用起来还

Xing Chen 597 Nov 22, 2022
A number of preset loading indicators created with SwiftUI

ActivityIndicatorView A number of preset loading indicators created with SwiftUI We are a development agency building phenomenal apps. Usage Create an

Exyte 956 Dec 26, 2022
Play BreakOut while loading - A playable pull to refresh view using SpriteKit

BreakOutToRefresh Play BreakOut while loading - A playable pull to refresh view using SpriteKit BreakOutToRefresh uses SpriteKit to add a playable min

Dominik Hauser 2.5k Jan 5, 2023
Beautiful animated placeholders for showing loading of data

KALoader Create breautiful animated placeholders for showing loading of data. You can change colors like you want. Swift 4 compatible. Usage To add an

Kirill Avery 105 May 2, 2022
An easy way to add a shimmering effect to any view with just one line of code. It is useful as an unobtrusive loading indicator.

LoadingShimmer An easy way to add a shimmering effect to any view with just single line of code. It is useful as an unobtrusive loading indicator. Thi

Jogendra 1.4k Jan 4, 2023
A collection of awesome loading animations

NVActivityIndicatorView ⚠️ Check out LoaderUI (ready to use with Swift Package Mananger supported) for SwiftUI implementation of this. ?? Introduction

Vinh Nguyen 10.3k Dec 27, 2022
Windless makes it easy to implement invisible layout loading view.

Windless Windless makes it easy to implement invisible layout loading view. Contents Requirements Installation Usage Looks Credits Communication Licen

ArLupin 940 Dec 22, 2022
Zeplin component preview for your SwiftUI views

A Zeplin component preview for your SwiftUI views. You can use Zeplin components instead of real views within your app until you implement them.

Danis Tazetdinov 4 Sep 1, 2022
A SwiftUI Views for wrapping HStack elements into multiple lines

SwiftUI WrappingStack A SwiftUI Views for wrapping HStack elements into multiple lines. List of supported views WrappingHStack - provides HStack that

Denis 50 Jan 6, 2023