Issue #763
I usually break down a big struct into smaller views and extensions. For example I have a ClipboardCell
that has a lot of onReceive
so I want to move these to another component.
One way to do that is to extend ClipboardCell
struct ClipboardCell: View {
let isSelected: Bool
@State var showsPreview: Bool
@State var showsViewRaw: Bool
let onCopy: () -> Void
let onDelete: () -> Void
}
extension ClipboardCell {
func onReceiveKeyboard() -> some View {
self.onReceive(
NotificationCenter.default
.publisher(for: .didKeyboardCopyItem)
.receive(on: RunLoop.main),
perform: { note in
onCopy()
}
)
}
}
but then when we want to use this, we get some View has no member onReceiveKeyboard
as self
after some Swift modifier becomes some View
, unless we call onReceiveKeyboard
first
struct ClipboardCell: View {
var body: some View {
self
.padding()
.onReceiveKeyboard()
}
}
Use ViewModifier
The SwiftUI is to use ViewModifier
where we can inject Binding and functions
struct ClipboardCellOnKeyboardModifier: ViewModifier {
let isSelected: Bool
@Binding var showsPreview: Bool
@Binding var showsViewRaw: Bool
let onCopy: () -> Void
let onDelete: () -> Void
func body(content: Content) -> some View {
content.onReceive(
NotificationCenter.default
.publisher(for: .didKeyboardCopyItem)
.receive(on: RunLoop.main),
perform: { _ in
guard isSelected else { return }
onCopy()
}
)
}
}
Then we can consume it and pass parameters
struct ClipboardCell: View {
var body: some View {
self
.padding()
.modifier(
ClipboardCellOnKeyboardModifier(
showsPreview: Binding<Bool>(get: {}, set: {}) ,
showsViewRaw: Binding<Bool>(get: {}, set: {})
)
)
}
}
Pass State and Binding
For now SwiftUI seems to have a bug that ViewModifier does not listen to onReceive
, we can extend generic View
and pass parameters instead
extension View {
func onClipboardCellReceiveKeyboard(
isSelected: Bool,
showsPreview: Binding<Bool>,
showsViewRaw: Binding<Bool>,
onCopy: () -> Void,
onDelete: () -> Void
) -> some View {
self.onReceive(
NotificationCenter.default
.publisher(for: .didKeyboardCopyItem)
.receive(on: RunLoop.main),
perform: { _ in
guard isSelected else { return }
onCopy()
}
)
Use ObservableObject
Another way is to use an ObservableObject
and encapsulate logic and state in there, and share this across views that want to consume this set of data, just like a ViewModel
import SwiftUI
final class ItemsHolder: ObservableObject {
@Published var items: [ClipboardItem] = []
@Published var selectedItems = Set<ClipboardItem>()
@Published var agos: [UUID: String] = [:]
func updateAgos() {
agos.removeAll()
for item in items {
agos[item.id] = Formattes.ago(date: item.createdAt)
}
}
func update(items: [ClipboardItem]) {
self.items = items
.sorted(by: { $0.createdAt > $1.createdAt })
updateAgos()
}
}
struct ClipboardCell: View {
@StateObject var itemsHolder = ItemsHolder()
var body: some View {
list.onReceive(
NotificationCenter.default
.publisher(for: .didKeyboardCopyItem)
.receive(on: RunLoop.main),
perform: { note in
itemsHolder.onCopy()
}
)
}
}