Last active
July 5, 2024 15:59
-
-
Save rl-pavel/75edc2ca2a9e1547a5e680a754fae31f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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