Skip to content

Commit

Permalink
implement empty view. close #2
Browse files Browse the repository at this point in the history
  • Loading branch information
jessesquires committed May 23, 2024
1 parent b89fd4c commit 2a5bb7c
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 5 deletions.
4 changes: 4 additions & 0 deletions Example/ExampleApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
0B0B5AA52BFF0FDB0037BC02 /* EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0B5AA42BFF0FDB0037BC02 /* EmptyView.swift */; };
0B31B9482BCC62A5006F2078 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0B31B9472BCC62A5006F2078 /* Assets.xcassets */; };
0B31B94B2BCC62A5006F2078 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = 0B31B94A2BCC62A5006F2078 /* Base */; };
0B31B9722BCC6302006F2078 /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B31B9702BCC62FD006F2078 /* ExampleTests.swift */; };
Expand Down Expand Up @@ -84,6 +85,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
0B0B5AA42BFF0FDB0037BC02 /* EmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyView.swift; sourceTree = "<group>"; };
0B31B93B2BCC62A4006F2078 /* ExampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
0B31B9472BCC62A5006F2078 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
0B31B94A2BCC62A5006F2078 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
Expand Down Expand Up @@ -214,6 +216,7 @@
0B31B9802BCC6349006F2078 /* PersonCellViewModelList.swift */,
0B31B97F2BCC6349006F2078 /* ColorCellViewModelList.swift */,
0B31B9812BCC6349006F2078 /* ListViewController.swift */,
0B0B5AA42BFF0FDB0037BC02 /* EmptyView.swift */,
);
path = List;
sourceTree = "<group>";
Expand Down Expand Up @@ -450,6 +453,7 @@
0B31B9992BCC6349006F2078 /* FooterView.swift in Sources */,
0B31B99B2BCC6349006F2078 /* ColorCellViewModelList.swift in Sources */,
0B31B9932BCC6349006F2078 /* ColorModel.swift in Sources */,
0B0B5AA52BFF0FDB0037BC02 /* EmptyView.swift in Sources */,
0B31B99D2BCC6349006F2078 /* ListViewController.swift in Sources */,
0B31B9972BCC6349006F2078 /* FavoriteBadgeView.swift in Sources */,
0B31B9A32BCC6349006F2078 /* GridViewController.swift in Sources */,
Expand Down
5 changes: 2 additions & 3 deletions Example/Sources/Grid/GridViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ final class GridViewController: ExampleViewController, CellEventCoordinator {
lazy var driver = CollectionViewDriver(
view: self.collectionView,
layout: self.makeLayout(),
cellEventCoordinator: self,
animateUpdates: true,
didUpdate: nil
emptyViewProvider: sharedEmptyViewProvider,
cellEventCoordinator: self
)

override var model: Model {
Expand Down
36 changes: 36 additions & 0 deletions Example/Sources/List/EmptyView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Created by Jesse Squires
// https://www.jessesquires.com
//
// Documentation
// https://jessesquires.github.io/ReactiveCollectionsKit
//
// GitHub
// https://github.com/jessesquires/ReactiveCollectionsKit
//
// Copyright © 2019-present Jesse Squires
//

import Foundation
import ReactiveCollectionsKit
import UIKit

let sharedEmptyViewProvider = EmptyViewProvider {
if #available(iOS 17.0, *) {
var config = UIContentUnavailableConfiguration.empty()
config.text = "No Content"
config.secondaryText = "The list is empty! Nothing to see here."
config.image = UIImage(systemName: "exclamationmark.triangle.fill")
var background = UIBackgroundConfiguration.clear()
background.backgroundColor = .tertiarySystemBackground
config.background = background
return UIContentUnavailableView(configuration: config)
}

let label = UILabel()
label.text = "No Content"
label.font = UIFont.preferredFont(forTextStyle: .title1)
label.textAlignment = .center
label.backgroundColor = .secondarySystemBackground
return label
}
1 change: 1 addition & 0 deletions Example/Sources/List/ListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ final class ListViewController: ExampleViewController, CellEventCoordinator {
lazy var driver = CollectionViewDriver(
view: self.collectionView,
layout: self.makeLayout(),
emptyViewProvider: sharedEmptyViewProvider,
cellEventCoordinator: self,
animateUpdates: true
) { [unowned self] driver in
Expand Down
4 changes: 4 additions & 0 deletions ReactiveCollectionsKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
0B01476F2BBCF0330041D432 /* Collection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B01476E2BBCF0330041D432 /* Collection+Extensions.swift */; };
0B0B5AA32BFF0BDB0037BC02 /* EmptyViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B0B5AA22BFF0BDB0037BC02 /* EmptyViewProvider.swift */; };
0B25D73A2BFAD31C00EB3664 /* UICollectionView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B25D7392BFAD31C00EB3664 /* UICollectionView+Extensions.swift */; };
0B31B9312BCC5B66006F2078 /* CellEventCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B31B9302BCC5B66006F2078 /* CellEventCoordinator.swift */; };
0B8C6CEC267ED08E0042DFE2 /* SupplementaryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B8C6CEB267ED08E0042DFE2 /* SupplementaryViewModel.swift */; };
Expand Down Expand Up @@ -51,6 +52,7 @@

/* Begin PBXFileReference section */
0B01476E2BBCF0330041D432 /* Collection+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Extensions.swift"; sourceTree = "<group>"; };
0B0B5AA22BFF0BDB0037BC02 /* EmptyViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyViewProvider.swift; sourceTree = "<group>"; };
0B25D7392BFAD31C00EB3664 /* UICollectionView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Extensions.swift"; sourceTree = "<group>"; };
0B31B9302BCC5B66006F2078 /* CellEventCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellEventCoordinator.swift; sourceTree = "<group>"; };
0B8C6CEB267ED08E0042DFE2 /* SupplementaryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupplementaryViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -160,6 +162,7 @@
88C02078236CBCB000B43713 /* DiffableDataSource.swift */,
0BC546EE267923BA00E36D30 /* DiffableSnapshot.swift */,
88FA49302363BC5F0061F8B2 /* DiffableViewModel.swift */,
0B0B5AA22BFF0BDB0037BC02 /* EmptyViewProvider.swift */,
88FA48FD2363A7880061F8B2 /* SectionViewModel.swift */,
0BC1EC29267FC3A000FF774F /* SupplementaryFooterViewModel.swift */,
0BC1EC27267FC37200FF774F /* SupplementaryHeaderViewModel.swift */,
Expand Down Expand Up @@ -330,6 +333,7 @@
0B8C6CEC267ED08E0042DFE2 /* SupplementaryViewModel.swift in Sources */,
88C02079236CBCB000B43713 /* DiffableDataSource.swift in Sources */,
0B31B9312BCC5B66006F2078 /* CellEventCoordinator.swift in Sources */,
0B0B5AA32BFF0BDB0037BC02 /* EmptyViewProvider.swift in Sources */,
88FA49312363BC5F0061F8B2 /* DiffableViewModel.swift in Sources */,
0BC1EC2E267FF46E00FF774F /* ViewRegistration.swift in Sources */,
0BC1EC2C267FE71900FF774F /* ViewRegistrationProvider.swift in Sources */,
Expand Down
55 changes: 53 additions & 2 deletions Sources/CollectionViewDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public final class CollectionViewDriver: NSObject {
}
}

private let _emptyViewProvider: EmptyViewProvider?

private var _currentEmptyView: UIView?

// Avoiding a strong reference to prevent a possible retain cycle.
// This is typically the view controller that owns `self` (the driver).
// The caller is responsible for retaining this object for the lifetime of the driver.
Expand All @@ -51,31 +55,34 @@ public final class CollectionViewDriver: NSObject {
// MARK: Init

/// Initializes a new `CollectionViewDriver`.
///
///
/// - Parameters:
/// - view: The collection view.
/// - layout: The collection view layout.
/// - viewModel: The collection view model.
/// - emptyViewProvider: An empty view provider.
/// - cellEventCoordinator: The cell event coordinator,
/// if you wish to handle cell events outside of your cell view models.
/// **Note: This object is not retained by the driver.**
/// - animateUpdates: Specifies whether or not to animate updates.
/// Pass `true` to animate, `false` otherwise.
/// - didUpdate: A closure to call when the driver finishes diffing and updating the collection view.
///
///
/// - Warning: The driver **does not** retain the `cellEventCoordinator`,
/// because this object is typically the view controller that owns the driver.
/// Thus, the caller is responsible for retaining and keeping alive the `cellEventCoordinator`
/// for the entire lifetime of the driver.
public init(view: UICollectionView,
layout: UICollectionViewCompositionalLayout,
viewModel: CollectionViewModel = CollectionViewModel(),
emptyViewProvider: EmptyViewProvider? = nil,
cellEventCoordinator: CellEventCoordinator?,
animateUpdates: Bool = true,
didUpdate: DidUpdate? = nil) {
self.view = view
self.view.collectionViewLayout = layout
self.viewModel = viewModel
self._emptyViewProvider = emptyViewProvider
self._cellEventCoordinator = cellEventCoordinator
self.animateUpdates = animateUpdates
self._didUpdate = didUpdate
Expand Down Expand Up @@ -158,6 +165,50 @@ public final class CollectionViewDriver: NSObject {

private func _handleDidUpdate() {
self._didUpdate?(self)
self._displayEmptyViewIfNeeded()
}

private func _displayEmptyViewIfNeeded() {
if self.viewModel.isEmpty {
guard self._currentEmptyView == nil else { return }
guard let emptyView = self._emptyViewProvider?.view else { return }

emptyView.frame = self.view.frame
emptyView.translatesAutoresizingMaskIntoConstraints = false
emptyView.alpha = 0
self.view.superview?.addSubview(emptyView)
NSLayoutConstraint.activate([
emptyView.topAnchor.constraint(equalTo: self.view.superview!.topAnchor),
emptyView.bottomAnchor.constraint(equalTo: self.view.superview!.bottomAnchor),
emptyView.leadingAnchor.constraint(equalTo: self.view.superview!.leadingAnchor),
emptyView.trailingAnchor.constraint(equalTo: self.view.superview!.trailingAnchor)
])
self._currentEmptyView = emptyView
self._animateEmptyView(isHidden: false)
} else {
self._animateEmptyView(isHidden: true)
}
}

private func _animateEmptyView(isHidden: Bool) {
guard self.animateUpdates else {
if isHidden {
self._currentEmptyView?.removeFromSuperview()
self._currentEmptyView = nil
} else {
self._currentEmptyView?.alpha = 1
}
return
}

UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) {
self._currentEmptyView?.alpha = isHidden ? 0 : 1
} completion: { _ in
if isHidden {
self._currentEmptyView?.removeFromSuperview()
self._currentEmptyView = nil
}
}
}

private func _cellProvider(
Expand Down
34 changes: 34 additions & 0 deletions Sources/EmptyViewProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// Created by Jesse Squires
// https://www.jessesquires.com
//
// Documentation
// https://jessesquires.github.io/ReactiveCollectionsKit
//
// GitHub
// https://github.com/jessesquires/ReactiveCollectionsKit
//
// Copyright © 2019-present Jesse Squires
//

import Foundation
import UIKit

/// Provides an "empty state" or "no content" view for a collection view.
public struct EmptyViewProvider {

/// A closure that returns the view.
public let viewBuilder: () -> UIView

/// The empty view.
public var view: UIView {
viewBuilder()
}

/// Initializes an `EmptyViewProvider` with the given closure.
///
/// - Parameter viewBuilder: A closure that creates and returns the empty view.
public init(_ viewBuilder: @escaping () -> UIView) {
self.viewBuilder = viewBuilder
}
}

0 comments on commit 2a5bb7c

Please sign in to comment.