Add SampleApp2 (#3)
This commit is contained in:
parent
9c84e36de7
commit
c8cdb88be6
|
@ -42,3 +42,40 @@ xcodeproj(
|
||||||
top_level_target(":SampleApp", target_environments = ["simulator"]),
|
top_level_target(":SampleApp", target_environments = ["simulator"]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "SampleAppLib2",
|
||||||
|
module_name = "SampleAppLib2",
|
||||||
|
srcs = glob([
|
||||||
|
"SampleApp2/**/*.swift",
|
||||||
|
]),
|
||||||
|
deps = [
|
||||||
|
"//:ListDiffUI"
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
ios_application(
|
||||||
|
name = "SampleApp2",
|
||||||
|
bundle_id = "com.siyuyue.listdiffui.sampleapp",
|
||||||
|
families = [
|
||||||
|
"iphone",
|
||||||
|
"ipad",
|
||||||
|
],
|
||||||
|
infoplists = [
|
||||||
|
"SampleApp2/Info.plist",
|
||||||
|
],
|
||||||
|
launch_storyboard = "SampleApp2/LaunchScreen.storyboard",
|
||||||
|
minimum_os_version = "14.0",
|
||||||
|
deps = [
|
||||||
|
":SampleAppLib2",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
xcodeproj(
|
||||||
|
name = "SampleAppProject2",
|
||||||
|
project_name = "SampleApp2",
|
||||||
|
build_mode = "bazel",
|
||||||
|
top_level_targets = [
|
||||||
|
top_level_target(":SampleApp2", target_environments = ["simulator"]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 873 KiB |
|
@ -22,10 +22,6 @@ final class ButtonCell: ListCell {
|
||||||
button.backgroundColor = .blue
|
button.backgroundColor = .blue
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
||||||
func configure(viewModel: ButtonViewModel, viewState: ListViewStateNone) {
|
|
||||||
button.setTitle(viewModel.title, for: .normal)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ButtonCellController: ListCellController<
|
final class ButtonCellController: ListCellController<
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 671 KiB |
|
@ -0,0 +1,29 @@
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
// Override point for customization after application launch.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: UISceneSession Lifecycle
|
||||||
|
|
||||||
|
func application(
|
||||||
|
_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession,
|
||||||
|
options: UIScene.ConnectionOptions
|
||||||
|
) -> UISceneConfiguration {
|
||||||
|
// Called when a new scene session is being created.
|
||||||
|
// Use this method to select a configuration to create the new scene with.
|
||||||
|
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||||
|
}
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||||
|
// Called when the user discards a scene session.
|
||||||
|
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||||
|
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import ListDiffUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct EndOfListViewModel: ListViewModel & Equatable {
|
||||||
|
|
||||||
|
var identifier: String {
|
||||||
|
"EndOfList"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class EndOfListCell: ListCell {
|
||||||
|
|
||||||
|
lazy var button: UIButton = {
|
||||||
|
let button = UIButton()
|
||||||
|
contentView.addSubview(button)
|
||||||
|
button.frame = CGRect(x: contentView.bounds.midX - 80, y: 5, width: 160, height: 30)
|
||||||
|
button.backgroundColor = .blue
|
||||||
|
button.setTitle("Load More", for: .normal)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol EndOfListCellControllerDelegate {
|
||||||
|
func didTapLoadMore()
|
||||||
|
}
|
||||||
|
|
||||||
|
final class EndOfListCellController: ListCellController<
|
||||||
|
EndOfListViewModel,
|
||||||
|
ListViewStateNone,
|
||||||
|
EndOfListCell
|
||||||
|
>, ListCellControllerWithDelegate
|
||||||
|
{
|
||||||
|
|
||||||
|
typealias DelegateType = EndOfListCellControllerDelegate
|
||||||
|
|
||||||
|
override func itemSize(containerSize: CGSize) -> CGSize {
|
||||||
|
return CGSize(width: containerSize.width, height: 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didMount(onCell cell: EndOfListCell) {
|
||||||
|
cell.button.removeTarget(nil, action: nil, for: .allEvents)
|
||||||
|
cell.button.addTarget(self, action: #selector(didTap), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func didTap() {
|
||||||
|
delegate?.didTapLoadMore()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIWindowSceneSessionRoleApplication</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UISceneConfigurationName</key>
|
||||||
|
<string>Default Configuration</string>
|
||||||
|
<key>UISceneDelegateClassName</key>
|
||||||
|
<string>SampleAppLib2.SceneDelegate</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
|
@ -0,0 +1,44 @@
|
||||||
|
import ListDiffUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct LoadingSpinnerViewModel: ListViewModel, Equatable {
|
||||||
|
|
||||||
|
var identifier: String {
|
||||||
|
"loadingSpinner"
|
||||||
|
}
|
||||||
|
|
||||||
|
var height: CGFloat
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LoadingSpinnerCell: ListCell {
|
||||||
|
|
||||||
|
lazy var loadingSpinner: UIActivityIndicatorView = {
|
||||||
|
var loadingSpinner = UIActivityIndicatorView()
|
||||||
|
contentView.addSubview(loadingSpinner)
|
||||||
|
return loadingSpinner
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LoadingSpinnerCellController: ListCellController<
|
||||||
|
LoadingSpinnerViewModel,
|
||||||
|
ListViewStateNone,
|
||||||
|
LoadingSpinnerCell
|
||||||
|
>
|
||||||
|
{
|
||||||
|
|
||||||
|
override func itemSize(containerSize: CGSize) -> CGSize {
|
||||||
|
return CGSize(width: containerSize.width, height: viewModel.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func cellDidLayoutSubviews(cell: LoadingSpinnerCell) {
|
||||||
|
cell.loadingSpinner.frame = cell.contentView.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didAppear(cell: LoadingSpinnerCell) {
|
||||||
|
cell.loadingSpinner.startAnimating()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func willDisappear(cell: LoadingSpinnerCell?) {
|
||||||
|
cell?.loadingSpinner.stopAnimating()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import Combine
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct ItemModel {
|
||||||
|
|
||||||
|
enum Item {
|
||||||
|
case text(String)
|
||||||
|
case textWithImage(String, UIImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: String
|
||||||
|
var item: Item
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DataModel {
|
||||||
|
var isLoading: Bool
|
||||||
|
var hasMore: Bool
|
||||||
|
var items: [ItemModel]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ModelProvider {
|
||||||
|
|
||||||
|
private var isLoading = false
|
||||||
|
private var hasMore = true
|
||||||
|
private var items: [ItemModel] = []
|
||||||
|
|
||||||
|
let dataModelPublisher: AnyPublisher<DataModel, Error>
|
||||||
|
private let dataModelSubject: CurrentValueSubject<DataModel, Error>
|
||||||
|
|
||||||
|
init() {
|
||||||
|
dataModelSubject = CurrentValueSubject(DataModel(isLoading: isLoading, hasMore: hasMore, items: items))
|
||||||
|
dataModelPublisher = dataModelSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMore() {
|
||||||
|
guard !isLoading else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isLoading = true
|
||||||
|
publishDataModel()
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { [weak self] in
|
||||||
|
guard let self = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.isLoading = false
|
||||||
|
for _ in 0..<3 {
|
||||||
|
switch Int.random(in: 0...1) {
|
||||||
|
case 0:
|
||||||
|
self.items.append(ItemModel(id: "\(self.items.count)", item: .text("Text")))
|
||||||
|
case 1:
|
||||||
|
self.items.append(
|
||||||
|
ItemModel(id: "\(self.items.count)", item: .textWithImage("Text with image", randomImage())))
|
||||||
|
default:
|
||||||
|
assertionFailure("Should not get here.")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
self.publishDataModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func publishDataModel() {
|
||||||
|
dataModelSubject.send(DataModel(isLoading: isLoading, hasMore: hasMore, items: items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func randomImage() -> UIImage {
|
||||||
|
let colors: [UIColor] = [.red, .blue, .cyan, .green, .purple]
|
||||||
|
let color = colors.randomElement()!
|
||||||
|
let size = CGSize(width: 40, height: 40)
|
||||||
|
return UIGraphicsImageRenderer(size: size).image { rendererContext in
|
||||||
|
color.setFill()
|
||||||
|
rendererContext.fill(CGRect(origin: .zero, size: size))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
|
var window: UIWindow?
|
||||||
|
|
||||||
|
func scene(
|
||||||
|
_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions
|
||||||
|
) {
|
||||||
|
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
||||||
|
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||||
|
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||||
|
guard let windowScene = (scene as? UIWindowScene) else { return }
|
||||||
|
let viewController = ViewController()
|
||||||
|
window = UIWindow(windowScene: windowScene)
|
||||||
|
window?.rootViewController = viewController
|
||||||
|
window?.makeKeyAndVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidDisconnect(_ scene: UIScene) {
|
||||||
|
// Called as the scene is being released by the system.
|
||||||
|
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||||
|
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
||||||
|
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||||
|
// Called when the scene has moved from an inactive state to an active state.
|
||||||
|
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneWillResignActive(_ scene: UIScene) {
|
||||||
|
// Called when the scene will move from an active state to an inactive state.
|
||||||
|
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||||
|
// Called as the scene transitions from the background to the foreground.
|
||||||
|
// Use this method to undo the changes made on entering the background.
|
||||||
|
}
|
||||||
|
|
||||||
|
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||||
|
// Called as the scene transitions from the foreground to the background.
|
||||||
|
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||||
|
// to restore the scene back to its current state.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import ListDiffUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct TextViewModel: ListViewModel, Equatable {
|
||||||
|
|
||||||
|
var identifier: String
|
||||||
|
var text: String
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TextCell: ListCell {
|
||||||
|
|
||||||
|
lazy var label: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
contentView.addSubview(label)
|
||||||
|
label.frame = CGRect(x: 16, y: 0, width: contentView.bounds.width - 32, height: contentView.bounds.height)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
contentView.backgroundColor = .lightGray
|
||||||
|
contentView.layer.masksToBounds = true
|
||||||
|
contentView.layer.cornerRadius = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("Unimplemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TextCellController: ListCellController<
|
||||||
|
TextViewModel,
|
||||||
|
ListViewStateNone,
|
||||||
|
TextCell
|
||||||
|
>
|
||||||
|
{
|
||||||
|
|
||||||
|
override func itemSize(containerSize: CGSize) -> CGSize {
|
||||||
|
return CGSize(width: containerSize.width, height: 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func configureCell(cell: TextCell) {
|
||||||
|
cell.label.text = viewModel.text
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import ListDiffUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct TextWithImageViewModel: ListViewModel, Equatable {
|
||||||
|
|
||||||
|
var identifier: String
|
||||||
|
var text: String
|
||||||
|
var image: UIImage
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TextWithImageCell: ListCell {
|
||||||
|
|
||||||
|
lazy var label: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
contentView.addSubview(label)
|
||||||
|
label.frame = CGRect(x: 16, y: 80, width: contentView.bounds.width - 32, height: 40)
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var imageView: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
contentView.addSubview(imageView)
|
||||||
|
imageView.frame = CGRect(x: 0, y: 0, width: contentView.bounds.width, height: 80)
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
contentView.backgroundColor = .lightGray
|
||||||
|
contentView.layer.masksToBounds = true
|
||||||
|
contentView.layer.cornerRadius = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("Unimplemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TextWithImageCellController: ListCellController<
|
||||||
|
TextWithImageViewModel,
|
||||||
|
ListViewStateNone,
|
||||||
|
TextWithImageCell
|
||||||
|
>
|
||||||
|
{
|
||||||
|
|
||||||
|
override func itemSize(containerSize: CGSize) -> CGSize {
|
||||||
|
return CGSize(width: containerSize.width, height: 120)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func configureCell(cell: TextWithImageCell) {
|
||||||
|
cell.label.text = viewModel.text
|
||||||
|
cell.imageView.image = viewModel.image
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
import Combine
|
||||||
|
import ListDiffUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class ViewController: UIViewController {
|
||||||
|
|
||||||
|
private lazy var collectionView: UICollectionView = {
|
||||||
|
let flowLayout = UICollectionViewFlowLayout()
|
||||||
|
flowLayout.scrollDirection = .vertical
|
||||||
|
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
|
||||||
|
collectionView.contentInset = .init(top: 0, left: 32, bottom: 0, right: 32)
|
||||||
|
return collectionView
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var dataSource: ListDiffDataSource = {
|
||||||
|
let dataSource = ListDiffDataSource(collectionView: collectionView)
|
||||||
|
return dataSource
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let modelProvider = ModelProvider()
|
||||||
|
private var anyCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
view.backgroundColor = .white
|
||||||
|
|
||||||
|
view.addSubview(collectionView)
|
||||||
|
collectionView.frame = view.bounds
|
||||||
|
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
|
|
||||||
|
anyCancellable = modelProvider.dataModelPublisher.sink { _ in
|
||||||
|
} receiveValue: { [weak self] dataModel in
|
||||||
|
guard let self = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.dataSource.setRootSection(self.buildSection(dataModel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildSection(_ dataModel: DataModel) -> Section {
|
||||||
|
return CompositeSection(
|
||||||
|
ListRenderSection(dataModel.items) {
|
||||||
|
switch $0.item {
|
||||||
|
case let .text(text):
|
||||||
|
return ListSection<
|
||||||
|
TextViewModel,
|
||||||
|
TextCellController
|
||||||
|
>(TextViewModel(identifier: $0.id, text: text))
|
||||||
|
case let .textWithImage(text, image):
|
||||||
|
return ListSection<
|
||||||
|
TextWithImageViewModel,
|
||||||
|
TextWithImageCellController
|
||||||
|
>(TextWithImageViewModel(identifier: $0.id, text: text, image: image))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataModel.isLoading
|
||||||
|
? ListSection<
|
||||||
|
LoadingSpinnerViewModel,
|
||||||
|
LoadingSpinnerCellController
|
||||||
|
>(LoadingSpinnerViewModel(height: 40)) : nil,
|
||||||
|
!dataModel.isLoading && dataModel.hasMore
|
||||||
|
? ListSection<
|
||||||
|
EndOfListViewModel,
|
||||||
|
EndOfListCellController
|
||||||
|
>(EndOfListViewModel(), delegate: .init(object: self)) : nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ViewController: EndOfListCellControllerDelegate {
|
||||||
|
|
||||||
|
func didTapLoadMore() {
|
||||||
|
modelProvider.loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ItemModel: Identifiable {
|
||||||
|
|
||||||
|
var identifier: String {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
|
@ -170,7 +170,7 @@ Assuming we are building a ToDo list, to build it with ListDiffUI framework:
|
||||||
|
|
||||||
And that's it, ListDiffUI framework will take care of building the root section into an array of view models and updating UI accordingly.
|
And that's it, ListDiffUI framework will take care of building the root section into an array of view models and updating UI accordingly.
|
||||||
|
|
||||||
Refer to [the sample app](https://github.com/siyuyue/ListDiffUI/tree/main/Examples/SampleApp) for a slightly more complicated example, that also showcases a few additional features in the ListDiffUI framework, including:
|
Refer to [the sample apps](https://github.com/siyuyue/ListDiffUI/tree/main/Examples) for some examples, that showcases a few additional features in the ListDiffUI framework, including:
|
||||||
- Heterogeneous cells
|
- Heterogeneous cells
|
||||||
- Asynchronous diffing on background thread (This is a configuration on ListDiffDataSource)
|
- Asynchronous diffing on background thread (This is a configuration on ListDiffDataSource)
|
||||||
- Passing in delegate objects to each controller to handle data mutation
|
- Passing in delegate objects to each controller to handle data mutation
|
||||||
|
|
Loading…
Reference in New Issue