How to extend custom View in SwiftUI

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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()
}
)
}
}

Updated at 2021-01-29 12:51:52

Comments

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.