diff --git a/.gitignore b/.gitignore index 19ccb4f..5da9a19 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ DerivedData # Carthage Carthage/Checkouts Carthage/Build + +# Swift Package Manager +.build/ \ No newline at end of file diff --git a/Example.xcodeproj/project.pbxproj b/Example.xcodeproj/project.pbxproj index 82e57ca..d7d8594 100644 --- a/Example.xcodeproj/project.pbxproj +++ b/Example.xcodeproj/project.pbxproj @@ -32,6 +32,8 @@ 523646911C6F88CF00392180 /* StatefulViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6451661A64089800108EA3 /* StatefulViewController.swift */; }; 523646921C6F88CF00392180 /* StatefulViewControllerImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DC230DC1BB5BA810083B95A /* StatefulViewControllerImplementation.swift */; }; 523646931C6F88CF00392180 /* ViewStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6451671A64089800108EA3 /* ViewStateMachine.swift */; }; + D32EF03F2358D27F0001D9B5 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D32EF03E2358D27F0001D9B5 /* GradientView.swift */; }; + D377D48A21D5018800C93544 /* ForegroundViewStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D377D48921D5018800C93544 /* ForegroundViewStoreViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -97,6 +99,8 @@ 4DE62B0B19B65AF00021630A /* ErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; 4DE62B0C19B65AF00021630A /* LoadingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; 523646881C6F87B000392180 /* StatefulViewController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StatefulViewController.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D32EF03E2358D27F0001D9B5 /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; + D377D48921D5018800C93544 /* ForegroundViewStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForegroundViewStoreViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -199,6 +203,8 @@ 4DE62AE319B658610021630A /* ViewController.swift */, 4D0EA4711ECAF83E00139926 /* TableViewController.swift */, 4D0EA4731ECB052000139926 /* CollectionViewController.swift */, + D377D48921D5018800C93544 /* ForegroundViewStoreViewController.swift */, + D32EF03E2358D27F0001D9B5 /* GradientView.swift */, 4DE62B0819B65AF00021630A /* PlaceholderViews */, 4DE62AE519B658610021630A /* Main.storyboard */, 4D137EB619C1BE5700AC1050 /* LaunchScreen.xib */, @@ -331,21 +337,23 @@ attributes = { LastSwiftMigration = 0700; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 1000; + LastUpgradeCheck = 1020; ORGANIZATIONNAME = "Alexander Schuch"; TargetAttributes = { 4D6451451A64079200108EA3 = { CreatedOnToolsVersion = 6.2; - LastSwiftMigration = 1000; + LastSwiftMigration = 1020; }; 4D64514F1A64079200108EA3 = { CreatedOnToolsVersion = 6.2; - LastSwiftMigration = 1000; + DevelopmentTeam = M8F9QH57A6; + LastSwiftMigration = 1020; TestTargetID = 4DE62ADB19B658610021630A; }; 4DE62ADB19B658610021630A = { CreatedOnToolsVersion = 6.0; - LastSwiftMigration = 1000; + DevelopmentTeam = M8F9QH57A6; + LastSwiftMigration = 1020; }; 523646871C6F87B000392180 = { CreatedOnToolsVersion = 7.2.1; @@ -354,7 +362,7 @@ }; buildConfigurationList = 4DE62AD719B658610021630A /* Build configuration list for PBXProject "Example" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -433,7 +441,9 @@ files = ( 4DE62AE419B658610021630A /* ViewController.swift in Sources */, 4D0EA4721ECAF83E00139926 /* TableViewController.swift in Sources */, + D32EF03F2358D27F0001D9B5 /* GradientView.swift in Sources */, 4DE62B0D19B65AF00021630A /* BasicPlaceholderView.swift in Sources */, + D377D48A21D5018800C93544 /* ForegroundViewStoreViewController.swift in Sources */, 4DE62AE219B658610021630A /* AppDelegate.swift in Sources */, 4D0EA4741ECB052000139926 /* CollectionViewController.swift in Sources */, 4DE62B0F19B65AF00021630A /* ErrorView.swift in Sources */, @@ -509,13 +519,13 @@ ); INFOPLIST_FILE = StatefulViewController/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.aschuch.StatefulViewController; + PRODUCT_BUNDLE_IDENTIFIER = at.allaboutapps.StatefulViewController; PRODUCT_NAME = StatefulViewController; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -535,12 +545,12 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = StatefulViewController/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.aschuch.StatefulViewController; + PRODUCT_BUNDLE_IDENTIFIER = at.allaboutapps.StatefulViewController; PRODUCT_NAME = StatefulViewController; SKIP_INSTALL = YES; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -550,6 +560,7 @@ 4D6451611A64079300108EA3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + DEVELOPMENT_TEAM = M8F9QH57A6; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", @@ -560,7 +571,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.aschuch.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; }; name = Debug; @@ -569,13 +580,14 @@ isa = XCBuildConfiguration; buildSettings = { COPY_PHASE_STRIP = NO; + DEVELOPMENT_TEAM = M8F9QH57A6; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = StatefulViewControllerTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.aschuch.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; }; name = Release; @@ -584,6 +596,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -639,6 +652,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -687,11 +701,12 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = M8F9QH57A6; INFOPLIST_FILE = Example/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "com.aschuch.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = at.allaboutapps.StatefulViewControllerExample; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -699,11 +714,12 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = M8F9QH57A6; INFOPLIST_FILE = Example/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = "com.aschuch.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = at.allaboutapps.StatefulViewControllerExample; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -721,12 +737,12 @@ INFOPLIST_FILE = "$(SRCROOT)/StatefulViewController/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.aschuch.StatefulViewController; + PRODUCT_BUNDLE_IDENTIFIER = at.allaboutapps.StatefulViewController; PRODUCT_NAME = StatefulViewController; SDKROOT = appletvos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 10.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -747,12 +763,12 @@ INFOPLIST_FILE = "$(SRCROOT)/StatefulViewController/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.aschuch.StatefulViewController; + PRODUCT_BUNDLE_IDENTIFIER = at.allaboutapps.StatefulViewController; PRODUCT_NAME = StatefulViewController; SDKROOT = appletvos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 10.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; diff --git a/Example.xcodeproj/xcshareddata/xcschemes/StatefulViewController-iOS Tests.xcscheme b/Example.xcodeproj/xcshareddata/xcschemes/StatefulViewController-iOS Tests.xcscheme index 6122baa..78ba3e7 100644 --- a/Example.xcodeproj/xcshareddata/xcschemes/StatefulViewController-iOS Tests.xcscheme +++ b/Example.xcodeproj/xcshareddata/xcschemes/StatefulViewController-iOS Tests.xcscheme @@ -1,6 +1,6 @@ + + + + diff --git a/Example.xcodeproj/xcshareddata/xcschemes/StatefulViewController-iOS.xcscheme b/Example.xcodeproj/xcshareddata/xcschemes/StatefulViewController-iOS.xcscheme index 1ae15de..20d1d39 100644 --- a/Example.xcodeproj/xcshareddata/xcschemes/StatefulViewController-iOS.xcscheme +++ b/Example.xcodeproj/xcshareddata/xcschemes/StatefulViewController-iOS.xcscheme @@ -1,6 +1,6 @@ - - - - + + + - + + @@ -13,10 +12,6 @@ - - - - @@ -26,14 +21,14 @@ - + - + - - + + - + + @@ -64,7 +60,7 @@ - + @@ -79,11 +75,11 @@ - + diff --git a/Example/ForegroundViewStoreViewController.swift b/Example/ForegroundViewStoreViewController.swift new file mode 100644 index 0000000..2cb38ca --- /dev/null +++ b/Example/ForegroundViewStoreViewController.swift @@ -0,0 +1,146 @@ +// +// ForegroundViewStoreViewController.swift +// Example +// +// Created by Matthias Wagner on 27.12.18. +// Copyright © 2018 Alexander Schuch. All rights reserved. +// + +import UIKit +import StatefulViewController + +class ForegroundViewStoreViewController: UIViewController, StatefulViewController { + + // MARK: - IBOutlets + + @IBOutlet weak var tableView: UITableView! + + @IBOutlet weak var deleteButton: UIButton! + + @IBOutlet weak var addButton: UIButton! + + @IBOutlet weak var headerView: UIView! + + @IBOutlet weak var headerLabel: UILabel! + + @IBOutlet weak var overlayView: UIView! + + @IBOutlet weak var stateFullContainer: UIView! + + // MARK: - Properties + + private let refreshControl = UIRefreshControl() + + fileprivate var dataArray = [String]() + + // MARK: - LifeCylce + + override func viewDidLoad() { + super.viewDidLoad() + + // Setup refresh control + refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) + tableView.addSubview(refreshControl) + + let topInset = (navigationController?.navigationBar.frame.origin.y ?? 0) + + headerView.frame.origin.y + + headerView.frame.size.height + let insets = UIEdgeInsets(top: topInset, left: 0, bottom: 0, right: 0) + + // Setup placeholder views + loadingView = LoadingView(frame: view.frame).prepare(insets: insets) + emptyView = EmptyView(frame: view.frame).prepare(insets: insets) + let failureView = ErrorView(frame: view.frame) + failureView.tapGestureRecognizer.addTarget(self, action: #selector(refresh)) + errorView = failureView.prepare(insets: insets) + + foregroundViewStore = [ + .empty: [overlayView, headerLabel, addButton], + .error: [overlayView, headerLabel, addButton], + .loading: [addButton] + ] + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + setupInitialViewState() + refresh() + } + + // MARK: - Refresh + + @objc func refresh() { + if (lastState == .loading) { return } + + startLoading { + print("completaion startLoading -> loadingState: \(self.currentState.rawValue)") + } + print("startLoading -> loadingState: \(self.lastState.rawValue)") + + // Fake network call + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(3 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)) { + + // Success + self.dataArray = ["Merlot", "Sauvignon Blanc", "Blaufränkisch", "Pinot Nior"] + self.tableView.reloadData() + self.endLoading(error: nil, completion: { + print("completion endLoading -> loadingState: \(self.currentState.rawValue)") + }) + print("endLoading -> loadingState: \(self.lastState.rawValue)") + + // Error + //self.endLoading(error: NSError(domain: "foo", code: -1, userInfo: nil)) + + // No Content + //self.endLoading(error: nil) + + self.refreshControl.endRefreshing() + } + } + + // MARK: - IBActions + + @IBAction func onRefresh(_ sender: Any) { + refresh() + } + + @IBAction func onDeleteButton(_ sender: Any) { + guard let indexOfDeleteButton = foregroundViewStore?[.empty]?.firstIndex(of: deleteButton) else { return } + foregroundViewStore?[.empty]?.remove(at: indexOfDeleteButton) + } + + @IBAction func onAddButton(_ sender: Any) { + foregroundViewStore?[.empty]?.append(deleteButton) + } +} + + +extension ForegroundViewStoreViewController { + + func hasContent() -> Bool { + return false + } + + func handleErrorWhenContentAvailable(_ error: Error) { + let alertController = UIAlertController(title: "Ooops", message: "Something went wrong.", preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + self.present(alertController, animated: true, completion: nil) + } + +} + +// MARK: - UITableViewDataSource + +extension ForegroundViewStoreViewController: UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return dataArray.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "textCell", for: indexPath) + cell.textLabel?.text = dataArray[(indexPath as NSIndexPath).row] + return cell + } +} diff --git a/Example/GradientView.swift b/Example/GradientView.swift new file mode 100644 index 0000000..75b1364 --- /dev/null +++ b/Example/GradientView.swift @@ -0,0 +1,46 @@ +// +// GradientView.swift +// Example +// +// Created by Matthias Wagner on 17.10.19. +// Copyright © 2019 Alexander Schuch. All rights reserved. +// + +import UIKit + +@IBDesignable +class GradientView: UIView { + @IBInspectable var firstColor: UIColor = UIColor.clear { + didSet { + updateView() + } + } + @IBInspectable var secondColor: UIColor = UIColor.clear { + didSet { + updateView() + } + } + @IBInspectable var isHorizontal: Bool = true { + didSet { + updateView() + } + } + override class var layerClass: AnyClass { + get { + return CAGradientLayer.self + } + } + func updateView() { + let layer = self.layer as! CAGradientLayer + layer.colors = [firstColor, secondColor].map {$0.cgColor} + if (isHorizontal) { + layer.startPoint = CGPoint(x: 0, y: 0.5) + layer.endPoint = CGPoint (x: 1, y: 0.5) + } else { + layer.startPoint = CGPoint(x: 0.1, y: 0) + layer.endPoint = CGPoint (x: 0.9, y: 1) + } + + layer.opacity = 0.4 + } +} diff --git a/Example/Info.plist b/Example/Info.plist index 6905cc6..15e83e5 100644 --- a/Example/Info.plist +++ b/Example/Info.plist @@ -36,5 +36,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIUserInterfaceStyle + Light diff --git a/Example/PlaceholderViews/BasicPlaceholderView.swift b/Example/PlaceholderViews/BasicPlaceholderView.swift index e5c5440..d3b5fd0 100644 --- a/Example/PlaceholderViews/BasicPlaceholderView.swift +++ b/Example/PlaceholderViews/BasicPlaceholderView.swift @@ -7,10 +7,13 @@ // import UIKit +import StatefulViewController -class BasicPlaceholderView: UIView { +class BasicPlaceholderView: UIView, StatefulPlaceholderView { let centerView: UIView = UIView() + + private var insets: UIEdgeInsets = .zero override init(frame: CGRect) { super.init(frame: frame) @@ -40,4 +43,12 @@ class BasicPlaceholderView: UIView { addConstraint(centerConstraint) } + func prepare(insets: UIEdgeInsets = .zero) -> BasicPlaceholderView { + self.insets = insets + return self + } + + func placeholderViewInsets() -> UIEdgeInsets { + return insets + } } diff --git a/Example/PlaceholderViews/LoadingView.swift b/Example/PlaceholderViews/LoadingView.swift index 0a27370..73c5633 100644 --- a/Example/PlaceholderViews/LoadingView.swift +++ b/Example/PlaceholderViews/LoadingView.swift @@ -9,7 +9,7 @@ import UIKit import StatefulViewController -class LoadingView: BasicPlaceholderView, StatefulPlaceholderView { +class LoadingView: BasicPlaceholderView { let label = UILabel() @@ -34,9 +34,4 @@ class LoadingView: BasicPlaceholderView, StatefulPlaceholderView { centerView.addConstraints(vConstraintsLabel) centerView.addConstraints(vConstraintsActivity) } - - func placeholderViewInsets() -> UIEdgeInsets { - return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) - } - } diff --git a/Example/ViewController.swift b/Example/ViewController.swift index 4bd7f74..2e11d39 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -13,6 +13,8 @@ class ViewController: UIViewController, StatefulViewController { fileprivate var dataArray = [String]() private let refreshControl = UIRefreshControl() @IBOutlet weak var tableView: UITableView! + + var foregroundViewStore = [StatefulViewControllerState.empty: []] override func viewDidLoad() { super.viewDidLoad() @@ -31,7 +33,7 @@ class ViewController: UIViewController, StatefulViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + setupInitialViewState() refresh() } diff --git a/LICENSE b/LICENSE index 4fd2232..997ca96 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) Copyright (c) 2014 - 2017 Alexander Schuch (http://schuch.me) +Copyright (c) 2018 - 2019 all about apps GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..8455eab --- /dev/null +++ b/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version:5.0 + +import PackageDescription + +let package = Package( + name: "StatefulViewController", + platforms: [ + .macOS(.v10_12), + .iOS(.v11), + .tvOS(.v11), + .watchOS(.v3) + ], + products: [ + .library(name: "StatefulViewController", targets: ["StatefulViewController"]) + ], + dependencies: [ + ], + targets: [ + .target(name: "StatefulViewController", dependencies: [], path: "StatefulViewController/"), + .testTarget(name: "StatefulViewControllerTests", dependencies: [], path: "StatefulViewControllerTests/") + ], + swiftLanguageVersions: [.v5] +) diff --git a/README.md b/README.md index f178392..24865c8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/aschuch/StatefulViewController.svg)](https://travis-ci.org/aschuch/StatefulViewController) ![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat) -![Swift 3.0](https://img.shields.io/badge/Swift-3.0-orange.svg) +![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg) ![Platform](https://img.shields.io/badge/platform-iOS%20%7C%20tvOS-lightgrey.svg) A protocol to enable `UIViewController`s or `UIView`s to present placeholder views based on content, loading, error or empty states. @@ -30,6 +30,7 @@ Current Swift compatibility breakdown: | Swift Version | Framework Version | | ------------- | ----------------- | +| 5.0 | 5.x | | 4.2 | 4.x | | 3.0 - 4.1 | 3.x | | 2.3 | 2.x | @@ -130,6 +131,25 @@ class MyPlaceholderView: UIView, StatefulPlaceholderView { +### ForegroundViewStore + +Per default, StatefulViewController presents the placeholder views above all other views. In case other views, like some buttons, should be above the placholder in some states, you can assign a dictionary `[StatefulViewControllerState: Set]` to the `foregroundViewStore` property. + +```swift +foregroundViewStore = [ + .empty: [button1, button2], + .error: [button1] +] +``` + +You can also update the `foregroundViewStore` later: + +```swift +foregroundViewStore?[.empty]?.remove(button1) +foregroundViewStore?[.loading]?.insert(button3) +``` + + ### View State Machine @@ -158,25 +178,23 @@ stateMachine.transitionToState(.None, animated: true) { ## Installation -#### Carthage +#### Swift Package Manager (Recommended) -Add the following line to your [Cartfile](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md#cartfile). +Add the following dependency to your `Package.swift` file: ``` -github "aschuch/StatefulViewController" ~> 3.0 +.package(url: "https://github.com/allaboutapps/StatefulViewController.git", from: "5.1.0") ``` -Then run `carthage update`. - -#### CocoaPods +#### Carthage -Add the following line to your Podfile. +Add the following line to your [Cartfile](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md#cartfile). ``` -pod "StatefulViewController", "~> 3.0" +github "aschuch/StatefulViewController" ~> 5.0 ``` -Then run `pod install` with CocoaPods 0.36 or newer. +Then run `carthage update`. #### Manually diff --git a/StatefulViewController.podspec b/StatefulViewController.podspec deleted file mode 100644 index ce56a0f..0000000 --- a/StatefulViewController.podspec +++ /dev/null @@ -1,15 +0,0 @@ -Pod::Spec.new do |s| - s.name = "StatefulViewController" - s.version = "3.0" - s.summary = "Placeholder views based on content, loading, error or empty states" - s.description = "A view controller subclass that presents placeholder views based on content, loading, error or empty states." - s.homepage = "https://github.com/aschuch/StatefulViewController" - s.license = { :type => "MIT", :file => "LICENSE" } - s.author = { "Alexander Schuch" => "alexander@schuch.me" } - s.social_media_url = "http://twitter.com/schuchalexander" - s.ios.deployment_target = "8.0" - s.tvos.deployment_target = "9.0" - s.source = { :git => "https://github.com/aschuch/StatefulViewController.git", :tag => s.version } - s.requires_arc = true - s.source_files = 'StatefulViewController/*.swift' -end diff --git a/StatefulViewController/StatefulViewController.swift b/StatefulViewController/StatefulViewController.swift index ea43248..65dec40 100644 --- a/StatefulViewController/StatefulViewController.swift +++ b/StatefulViewController/StatefulViewController.swift @@ -42,7 +42,9 @@ public protocol StatefulViewController: class, BackingViewProvider { /// The empty view is shown when the `hasContent` method returns false var emptyView: UIView? { get set } - + /// This store includes the views, who should be in the front during the specific state (Key) + var foregroundViewStore: [StatefulViewControllerState: [UIView]]? { get set } + // MARK: Transitions /// Sets up the initial state of the view. diff --git a/StatefulViewController/StatefulViewControllerImplementation.swift b/StatefulViewController/StatefulViewControllerImplementation.swift index 26b9b27..202d346 100644 --- a/StatefulViewController/StatefulViewControllerImplementation.swift +++ b/StatefulViewController/StatefulViewControllerImplementation.swift @@ -58,7 +58,11 @@ extension StatefulViewController { get { return placeholderView(.empty) } set { setPlaceholderView(newValue, forState: .empty) } } - + + public var foregroundViewStore: [StatefulViewControllerState: [UIView]]? { + get { return getForegroundStore() } + set { setForegroundViewStore(newValue) } + } // MARK: Transitions @@ -118,6 +122,20 @@ extension StatefulViewController { fileprivate func setPlaceholderView(_ view: UIView?, forState state: StatefulViewControllerState) { stateMachine[state.rawValue] = view } + + fileprivate func getForegroundStore() -> [StatefulViewControllerState: [UIView]]? { + return stateMachine.foregroundViewStore + } + + fileprivate func setForegroundViewStore(_ store: [StatefulViewControllerState: [UIView]]?) { + var tempStore = store + + store?.forEach { (state, views) in + tempStore?[state]?.removeDuplicates() + } + + stateMachine.foregroundViewStore = tempStore + } } @@ -133,3 +151,17 @@ private func associatedObject(_ host: AnyObject, key: UnsafeRawPoi } return value! } + +extension Array where Element: Hashable { + func removingDuplicates() -> [Element] { + var addedDict = [Element: Bool]() + + return filter { + addedDict.updateValue(true, forKey: $0) == nil + } + } + + mutating func removeDuplicates() { + self = self.removingDuplicates() + } +} diff --git a/StatefulViewController/ViewStateMachine.swift b/StatefulViewController/ViewStateMachine.swift index cd1dca9..b79d31a 100644 --- a/StatefulViewController/ViewStateMachine.swift +++ b/StatefulViewController/ViewStateMachine.swift @@ -53,6 +53,23 @@ public class ViewStateMachine { /// The view that should act as the superview for any added views public let view: UIView + + /// This store includes the views, who should be in the front during the specific state (Key) + public var foregroundViewStore: [StatefulViewControllerState: [UIView]]? = nil { + didSet { + view.bringSubviewToFront(containerView) // Reset to the default order + + if foregroundViewStore != nil { + switch currentState { + case .none: break + case .view(let stateKey): + + // Bring all stored views for the state to the front + bringForegroundViewStoreToFront(for: stateKey) + } + } + } + } /// The current display state of views public fileprivate(set) var currentState: ViewStateMachineState = .none @@ -176,14 +193,17 @@ public class ViewStateMachine { newView.translatesAutoresizingMaskIntoConstraints = false containerView.addSubview(newView) - let metrics = ["top": insets.top, "bottom": insets.bottom, "left": insets.left, "right": insets.right] - let views = ["view": newView] - let hConstraints = NSLayoutConstraint.constraints(withVisualFormat: "|-left-[view]-right-|", options: [], metrics: metrics, views: views) - let vConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|-top-[view]-bottom-|", options: [], metrics: metrics, views: views) - containerView.addConstraints(hConstraints) - containerView.addConstraints(vConstraints) + NSLayoutConstraint.activate([ + newView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: insets.right), + newView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: insets.bottom), + newView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: insets.top), + newView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: insets.left) + ]) } + // Bring all views from the foregroundViewStore, stored for the specific state, to the front + bringForegroundViewStoreToFront(for: state) + let animations: () -> () = { if let newView = store[state] { newView.alpha = 1.0 @@ -203,6 +223,17 @@ public class ViewStateMachine { animateChanges(animated: animated, animations: animations, completion: animationCompletion) } + private func bringForegroundViewStoreToFront(for state: String) { + if let stateFulVCState = StatefulViewControllerState(rawValue: state), + let foregroundViewStore = foregroundViewStore, + let foregroundViews = foregroundViewStore[stateFulVCState] { + + for foregroundView in foregroundViews { + view.bringSubviewToFront(foregroundView) + } + } + } + fileprivate func hideAllViews(animated: Bool, completion: (() -> ())? = nil) { let store = viewStore