Skip to content

Instantly share code, notes, and snippets.

@rl-pavel
Last active July 5, 2024 15:59
Show Gist options
  • Save rl-pavel/75edc2ca2a9e1547a5e680a754fae31f to your computer and use it in GitHub Desktop.
Save rl-pavel/75edc2ca2a9e1547a5e680a754fae31f to your computer and use it in GitHub Desktop.
public struct PartialForEach<
Data: RandomAccessCollection & MutableCollection,
ID: Hashable,
Content: View,
More: View
> where Data.Index == Int {
@Binding var data: Data
let idKeyPath: KeyPath<Data.Element, ID>
let maxItems: Int
let collapsible: Bool
let content: (Binding<Data.Element>) -> Content
let more: (Binding<Bool>) -> More
@State private var isShowingAll: Bool = false
}
// MARK: - View
extension PartialForEach: View {
public var body: some View {
let enumeratedVisibleData = isShowingAll
? Array(zip(data.indices, data))
: Array(zip(data.indices, data).prefix(maxItems))
let elementKeyPath = \(index: Data.Index, element: Data.Element).element
let idKeyPath = elementKeyPath.appending(path: idKeyPath)
ForEach(enumeratedVisibleData, id: idKeyPath) { index, element in
content(Binding(get: { element }, set: { data[index] = $0 }))
}
if (!isShowingAll || collapsible) && data.count > maxItems {
more($isShowingAll)
}
}
}
// MARK: - Inits
public extension PartialForEach {
init(
data: Binding<Data>,
id: KeyPath<Data.Element, ID>,
maxItems: Int,
collapsible: Bool = false,
@ViewBuilder content: @escaping (Binding<Data.Element>) -> Content,
@ViewBuilder more: @escaping (Binding<Bool>) -> More
) {
self._data = data
self.maxItems = maxItems
self.collapsible = collapsible
self.idKeyPath = id
self.content = content
self.more = more
_isShowingAll = State(wrappedValue: data.wrappedValue.count <= maxItems)
}
init(
data: Binding<Data>,
maxItems: Int,
collapsible: Bool = false,
@ViewBuilder content: @escaping (Binding<Data.Element>) -> Content,
@ViewBuilder more: @escaping (Binding<Bool>) -> More
) where Data.Element: Identifiable, Data.Element.ID == ID {
self._data = data
self.maxItems = maxItems
self.collapsible = collapsible
self.idKeyPath = \Data.Element.id
self.content = content
self.more = more
_isShowingAll = State(wrappedValue: data.wrappedValue.count <= maxItems)
}
init(
data: Data,
id: KeyPath<Data.Element, ID>,
maxItems: Int,
collapsible: Bool = false,
@ViewBuilder content: @escaping (Data.Element) -> Content,
@ViewBuilder more: @escaping (Binding<Bool>) -> More
) {
self._data = .constant(data)
self.maxItems = maxItems
self.collapsible = collapsible
self.idKeyPath = id
self.content = { content($0.wrappedValue) }
self.more = more
_isShowingAll = State(wrappedValue: data.count <= maxItems)
}
init(
data: Data,
maxItems: Int,
collapsible: Bool = false,
@ViewBuilder content: @escaping (Data.Element) -> Content,
@ViewBuilder more: @escaping (Binding<Bool>) -> More
) where Data.Element: Identifiable, Data.Element.ID == ID {
self._data = .constant(data)
self.maxItems = maxItems
self.collapsible = collapsible
self.idKeyPath = \Data.Element.id
self.content = { content($0.wrappedValue) }
self.more = more
_isShowingAll = State(wrappedValue: data.count <= maxItems)
}
}
#Preview {
struct Preview: View {
struct Item: Identifiable, Equatable {
let id = UUID()
var isOn = false
}
class ViewModel: ObservableObject {
@Published var items: Array<Item>
init() {
items = (0 ..< 15).map { _ in Item() }
}
}
@StateObject var viewModel = ViewModel()
let constantItems = (0..<7).map { _ in Item() }
var body: some View {
ScrollView {
VStack(alignment: .leading) {
PartialForEach(constantItems, maxItems: 3) { item in
Text(item.id.uuidString)
} more: { isShowingAll in
Button {
isShowingAll.wrappedValue.toggle()
} label: {
Text(isShowingAll.wrappedValue ? "Hide" : "Show all")
.frame(maxWidth: .infinity)
}
}
Divider()
PartialForEach(
$viewModel.items,
maxItems: 5,
collapsible: true,
content: { item in
Toggle(item.wrappedValue.id.uuidString, isOn: item.isOn)
}, more: { isShowingAll in
Button {
isShowingAll.wrappedValue.toggle()
} label: {
Text(isShowingAll.wrappedValue ? "Hide" : "Show all")
.frame(maxWidth: .infinity)
}
}
)
}
.font(.footnote)
.padding(.horizontal)
}
}
}
return Preview()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment