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()
            }
        )
    }
}