diff --git a/README.md b/README.md index 7a47569..7fb9b7d 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,17 @@ Add this Swift package in Xcode using its Github repository url. (File > Swift P ## 🚀 How to use -You control the view through an instance of `ListService`. The service manages the current state and the items of the list. -Use it to append, update or remove items and to modify the state of the list. The `AdvancedList` view listens to the service and updates itself if needed. +The `AdvancedList` view is similar to the `List` and `ForEach` views. You have to pass data (`RandomAccessCollection`) and a view provider (`(Data.Element) -> some View`) to the initializer. In addition to the `List` view the `AdvancedList` expects a list state and corresponding views. +Modify your data anytime or hide an item through the content block if you like. The view is updated automatically 🎉. ```swift import AdvancedList -let listService = ListService() +@State private var listState: ListState = .items -AdvancedList(listService: listService, emptyStateView: { +AdvancedList(yourData, content: { item in + Text("Item") +}, listState: $listState, emptyStateView: { Text("No data") }, errorStateView: { error in Text(error.localizedDescription) @@ -28,11 +30,6 @@ AdvancedList(listService: listService, emptyStateView: { }, loadingStateView: { Text("Loading ...") }, pagination: .noPagination) - -listService.listState = .loading -// TODO: fetch your items -listService.appendItems(yourItems) -listService.listState = .items ``` ### 📄 Pagination @@ -87,38 +84,85 @@ private(set) lazy var pagination: AdvancedListPagination = { ### 📁 Move and 🗑️ delete items -You can define which actions your list should support through the `supportedListActions` property of your `ListService` instance. -Choose between `delete`, `move`, `moveAndDelete` and `none`. **The default is `none`.** +You can define which actions your list should support through the `onMoveAction` and `onDeleteAction` initializer parameters. +**Per default the move and delete functions are disabled if you skip the parameters.** ```swift -let listService = ListService() -listService.supportedListActions = .moveAndDelete(onMove: { indexSet, index in - // move me -}, onDelete: { indexSet in - // please delete me -}) +import AdvancedList + +@State private var listState: ListState = .items + +AdvancedList(yourData, content: { item in + Text("Item") +}, listState: $listState, onMoveAction: { (indexSet, index) in + // do something +}, onDeleteAction: { indexSet in + // do something +}, emptyStateView: { + Text("No data") +}, errorStateView: { error in + Text(error.localizedDescription) + .lineLimit(nil) +}, loadingStateView: { + Text("Loading ...") +}, pagination: .noPagination) ``` ### 🎛️ Filtering -The `AdvancedList` supports filtering (**disabled by default**). You only have to set the closure `excludeItem: (AnyListItem) -> Bool)` on your `ListService` instance. -`AnyListItem` gives you access to the item (`Any`). **Keep in mind that you have to cast this item to your custom type!** +**You can hide items in your list through the content block.** Only return a view in the content block if a specific condition is met. + +## 🎁 Example + +The following code shows how easy-to-use the view is: ```swift -let listService = ListService() -listService.excludeItem = { ($0.item as? YourItem).type == .xyz } +import AdvancedList + +@State private var listState: ListState = .items + +AdvancedList(yourData, content: { item in + Text("Item") +}, listState: $listState, emptyStateView: { + Text("No data") +}, errorStateView: { error in + VStack { + Text(error.localizedDescription) + .lineLimit(nil) + + Button(action: { + // do something + }) { + Text("Retry") + } + } +}, loadingStateView: { + Text("Loading ...") +}, pagination: .noPagination) ``` -## 🎁 Example +For more examples take a look at [AdvancedList-SwiftUI](https://github.com/crelies/AdvancedList-SwiftUI). -The following code shows how easy-to-use the view is: +## Migration 2.x -> 3.0 + +The `AdvancedList` was dramatically simplified and is now more like the `List` and `ForEach` SwiftUI views. + +1. Delete your list service instances and directly **pass your data to the list initializer** +2. Create your views through a content block (**initializer parameter**) instead of conforming your items to `View` directly (removed type erased wrapper `AnyListItem`) +3. Pass a list state binding to the initializer (**before:** the `ListService` managed the list state) +4. **Move and delete:** Instead of setting `AdvancedListActions` on your list service just pass a `onMoveAction` and/or `onDeleteAction` block to the initializer +**Before:** ```swift import AdvancedList let listService = ListService() -listService.appendItems(yourItems) -listService.listState = .items +listService.supportedListActions = .moveAndDelete(onMove: { (indexSet, index) in + // please move me +}, onDelete: { indexSet in + // please delete me +}) +listService.listState = .loading AdvancedList(listService: listService, emptyStateView: { Text("No data") @@ -136,6 +180,39 @@ AdvancedList(listService: listService, emptyStateView: { }, loadingStateView: { Text("Loading ...") }, pagination: .noPagination) + +listService.listState = .loading +// fetch your items ... +listService.appendItems(yourItems) +listService.listState = .items ``` -For more examples take a look at [AdvancedList-SwiftUI](https://github.com/crelies/AdvancedList-SwiftUI). +**After:** +```swift +import AdvancedList + +@State private var listState: ListState = .items + +AdvancedList(yourData, content: { item in + Text("Item") +}, listState: $listState, onMoveAction: { (indexSet, index) in + // move me +}, onDeleteAction: { indexSet in + // delete me +}, emptyStateView: { + Text("No data") +}, errorStateView: { error in + VStack { + Text(error.localizedDescription) + .lineLimit(nil) + + Button(action: { + // do something + }) { + Text("Retry") + } + } +}, loadingStateView: { + Text("Loading ...") +}, pagination: .noPagination) +``` diff --git a/Sources/AdvancedList/public/Models/AdvancedListActions.swift b/Sources/AdvancedList/public/Models/AdvancedListActions.swift deleted file mode 100644 index 9459047..0000000 --- a/Sources/AdvancedList/public/Models/AdvancedListActions.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// AdvancedListActions.swift -// -// -// Created by Christian Elies on 09.11.19. -// - -import Foundation - -public enum AdvancedListActions { - case delete(onDelete: (IndexSet) -> Void) - case move(onMove: (IndexSet, Int) -> Void) - case moveAndDelete(onMove: (IndexSet, Int) -> Void, onDelete: (IndexSet) -> Void) - case none -} diff --git a/Sources/AdvancedList/public/Models/AnyListItem.swift b/Sources/AdvancedList/public/Models/AnyListItem.swift deleted file mode 100644 index 7ed3856..0000000 --- a/Sources/AdvancedList/public/Models/AnyListItem.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// AnyListItem.swift -// AdvancedList -// -// Created by Christian Elies on 12.07.19. -// Copyright © 2019 Christian Elies. All rights reserved. -// - -import Foundation -import SwiftUI - -public struct AnyListItem: Identifiable, View { - public let item: Any - public let id: AnyHashable - public let body: AnyView - - public init(item: Item) where Item: View { - self.item = item - id = item.id - body = AnyView(item) - } -} diff --git a/Sources/AdvancedList/public/Services/ListService.swift b/Sources/AdvancedList/public/Services/ListService.swift deleted file mode 100644 index ba28a84..0000000 --- a/Sources/AdvancedList/public/Services/ListService.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// ListService.swift -// AdvancedList -// -// Created by Christian Elies on 01.07.19. -// Copyright © 2019 Christian Elies. All rights reserved. -// - -import Combine -import Foundation -import SwiftUI - -public final class ListService: NSObject, ObservableObject { - public let objectWillChange = PassthroughSubject() - - public private(set) var items: [AnyListItem] = [] { - didSet { - objectWillChange.send() - } - } - - public var listState: ListState = .items { - didSet { - objectWillChange.send() - } - } - - public var supportedListActions: AdvancedListActions = .none - public var excludeItem: (AnyListItem) -> Bool = { _ in false } - - public func appendItems(_ items: [Item]) where Item: View { - let anyListItems = items.map { AnyListItem(item: $0) } - self.items.append(contentsOf: anyListItems) - } - - public func updateItems(_ items: [Item]) where Item: View { - let anyListItems = items.map { AnyListItem(item: $0) } - for anyListItem in anyListItems { - guard let itemIndex = self.items.firstIndex(where: { $0.id == anyListItem.id }) else { - continue - } - - self.items[itemIndex] = anyListItem - } - } - - public func removeItems(_ items: [Item]) where Item: View { - let anyListItemsToRemove = items.map { AnyListItem(item: $0) } - self.items.removeAll(where: { item in - return anyListItemsToRemove.contains { item.id == $0.id } - }) - } - - public func removeAllItems() { - items.removeAll() - } -} diff --git a/Sources/AdvancedList/public/Views/AdvancedList.swift b/Sources/AdvancedList/public/Views/AdvancedList.swift index 68afcdd..9aaa93e 100644 --- a/Sources/AdvancedList/public/Views/AdvancedList.swift +++ b/Sources/AdvancedList/public/Views/AdvancedList.swift @@ -9,16 +9,27 @@ import ListPagination import SwiftUI -public struct AdvancedList : View { - @ObservedObject private var listService: ListService +public struct AdvancedList : View where Data.Element: Identifiable { + public typealias OnMoveAction = Optional<(IndexSet, Int) -> Void> + public typealias OnDeleteAction = Optional<(IndexSet) -> Void> + @ObservedObject private var pagination: AdvancedListPagination + private var data: Data + private var content: (Data.Element) -> Content + private var listState: Binding + private var onMoveAction: OnMoveAction = nil + private var onDeleteAction: OnDeleteAction = nil private let emptyStateView: () -> EmptyStateView private let errorStateView: (Error) -> ErrorStateView private let loadingStateView: () -> LoadingStateView @State private var isLastItem: Bool = false - - public init(listService: ListService, @ViewBuilder emptyStateView: @escaping () -> EmptyStateView, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView, pagination: AdvancedListPagination) { - self.listService = listService + + public init(_ data: Data, @ViewBuilder content: @escaping (Data.Element) -> Content, listState: Binding, onMoveAction: OnMoveAction = nil, onDeleteAction: OnDeleteAction = nil, @ViewBuilder emptyStateView: @escaping () -> EmptyStateView, @ViewBuilder errorStateView: @escaping (Error) -> ErrorStateView, @ViewBuilder loadingStateView: @escaping () -> LoadingStateView, pagination: AdvancedListPagination) { + self.data = data + self.content = content + self.listState = listState + self.onMoveAction = onMoveAction + self.onDeleteAction = onDeleteAction self.emptyStateView = emptyStateView self.errorStateView = errorStateView self.loadingStateView = loadingStateView @@ -27,110 +38,70 @@ public struct AdvancedList(_ item: Item) { - switch pagination.type { - case .lastItem: - if listService.items.isLastItem(item) { - pagination.shouldLoadNextPage() - } - - case .thresholdItem(let offset): - if listService.items.isThresholdItem(offset: offset, - item: item) { - pagination.shouldLoadNextPage() - } - case .noPagination: () - } - } - private func getListView() -> some View { - switch listService.supportedListActions { - case .delete(let onDelete): - return AnyView(List { - ForEach(listService.items) { item in - if !self.listService.excludeItem(item) { - self.getItemView(item) - } - }.onDelete { indexSet in - onDelete(indexSet) - } - }) - case .move(let onMove): - return AnyView(List { - ForEach(listService.items) { item in - if !self.listService.excludeItem(item) { - self.getItemView(item) - } - }.onMove { (indexSet, index) in - onMove(indexSet, index) - } - }) - case .moveAndDelete(let onMove, let onDelete): - return AnyView(List { - ForEach(listService.items) { item in - if !self.listService.excludeItem(item) { - self.getItemView(item) - } - }.onMove { (indexSet, index) in - onMove(indexSet, index) - }.onDelete { indexSet in - onDelete(indexSet) - } - }) - case .none: - return AnyView(List(listService.items) { item in - if !self.listService.excludeItem(item) { - self.getItemView(item) - } - }) + List { + ForEach(data) { item in + self.getItemView(item) + }.onMove(perform: self.onMoveAction) + .onDelete(perform: self.onDeleteAction) } } - private func getItemView(_ item: AnyListItem) -> some View { - item + private func getItemView(_ item: Data.Element) -> some View { + content(item) .onAppear { self.listItemAppears(item) - if self.listService.items.isLastItem(item) { + if self.data.isLastItem(item) { self.isLastItem = true } } } - + + private func listItemAppears(_ item: Data.Element) { + switch pagination.type { + case .lastItem: + if data.isLastItem(item) { + pagination.shouldLoadNextPage() + } + + case .thresholdItem(let offset): + if data.isThresholdItem(offset: offset, + item: item) { + pagination.shouldLoadNextPage() + } + case .noPagination: () + } + } + private func getPaginationStateView() -> some View { var paginationStateView = AnyView(EmptyView()) - + switch pagination.state { case .error(let error): paginationStateView = AnyView(pagination.errorView(error)) @@ -139,24 +110,31 @@ extension AdvancedList { case .loading: paginationStateView = AnyView(pagination.loadingView()) } - + return paginationStateView } } #if DEBUG struct AdvancedList_Previews : PreviewProvider { - private static let listService = ListService() + private struct MockItem: Identifiable { + let id: String = UUID().uuidString + } + + private static let items: [MockItem] = [] + @State private static var listState: ListState = .items static var previews: some View { NavigationView { - AdvancedList(listService: listService, emptyStateView: { + AdvancedList(items, content: { element in + Text(element.id) + }, listState: $listState, emptyStateView: { Text("No data") }, errorStateView: { error in VStack { Text(error.localizedDescription) .lineLimit(nil) - + Button(action: { // do something }) {