Skip to content

Commit 99ca3c9

Browse files
authored
Fix sidebar update pill cached popover flow (#2142)
* test: cover cached update pill first-click flow * fix: use cached sidebar update popover
1 parent 049d296 commit 99ca3c9

5 files changed

Lines changed: 286 additions & 92 deletions

File tree

Sources/Update/UpdatePill.swift

Lines changed: 134 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import AppKit
2-
import Bonsplit
32
import Foundation
43
import SwiftUI
54

@@ -13,36 +12,19 @@ struct UpdatePill: View {
1312
var body: some View {
1413
if model.showsPill {
1514
pillButton
16-
.popover(
17-
isPresented: $showPopover,
18-
attachmentAnchor: .rect(.bounds),
19-
arrowEdge: .top
20-
) {
21-
UpdatePopoverView(model: model)
15+
.background(UpdatePillPopoverAnchor(isPresented: $showPopover, model: model))
16+
.onChange(of: model.showsPill) { _, showsPill in
17+
if !showsPill {
18+
showPopover = false
19+
}
2220
}
2321
.transition(.opacity.combined(with: .scale(scale: 0.95)))
2422
}
2523
}
2624

2725
@ViewBuilder
2826
private var pillButton: some View {
29-
Button(action: {
30-
if model.showsDetectedBackgroundUpdate {
31-
if showPopover {
32-
showPopover = false
33-
} else {
34-
showPopover = true
35-
AppDelegate.shared?.checkForUpdatesInCustomUI()
36-
}
37-
return
38-
}
39-
if case .notFound(let notFound) = model.state {
40-
model.state = .idle
41-
notFound.acknowledgement()
42-
} else {
43-
showPopover.toggle()
44-
}
45-
}) {
27+
Button(action: handleTap) {
4628
HStack(spacing: 6) {
4729
UpdateBadge(model: model)
4830
.frame(width: 14, height: 14)
@@ -68,13 +50,141 @@ struct UpdatePill: View {
6850
.accessibilityIdentifier("UpdatePill")
6951
}
7052

53+
private func handleTap() {
54+
if model.showsDetectedBackgroundUpdate {
55+
if model.hasCachedDetectedUpdateDetails {
56+
showPopover.toggle()
57+
} else if showPopover {
58+
showPopover = false
59+
} else {
60+
showPopover = true
61+
AppDelegate.shared?.checkForUpdatesInCustomUI()
62+
}
63+
return
64+
}
65+
66+
if case .notFound(let notFound) = model.state {
67+
model.state = .idle
68+
notFound.acknowledgement()
69+
} else {
70+
showPopover.toggle()
71+
}
72+
}
73+
7174
private var textWidth: CGFloat? {
7275
let attributes: [NSAttributedString.Key: Any] = [.font: textFont]
7376
let size = (model.maxWidthText as NSString).size(withAttributes: attributes)
7477
return size.width
7578
}
7679
}
7780

81+
private struct UpdatePillPopoverAnchor: NSViewRepresentable {
82+
@Binding var isPresented: Bool
83+
@ObservedObject var model: UpdateViewModel
84+
85+
func makeNSView(context: Context) -> NSView {
86+
let view = NSView()
87+
context.coordinator.anchorView = view
88+
return view
89+
}
90+
91+
func updateNSView(_ nsView: NSView, context: Context) {
92+
let coordinator = context.coordinator
93+
context.coordinator.anchorView = nsView
94+
context.coordinator.updateRootView(
95+
AnyView(
96+
UpdatePopoverView(model: model) {
97+
[weak coordinator] in
98+
coordinator?.closeFromContent()
99+
}
100+
)
101+
)
102+
103+
if isPresented {
104+
context.coordinator.present()
105+
} else {
106+
context.coordinator.dismiss()
107+
}
108+
}
109+
110+
func makeCoordinator() -> Coordinator {
111+
Coordinator(isPresented: $isPresented)
112+
}
113+
114+
static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) {
115+
coordinator.dismiss()
116+
}
117+
118+
final class Coordinator: NSObject, NSPopoverDelegate {
119+
@Binding var isPresented: Bool
120+
121+
weak var anchorView: NSView?
122+
private let hostingController = NSHostingController(rootView: AnyView(EmptyView()))
123+
private var popover: NSPopover?
124+
125+
init(isPresented: Binding<Bool>) {
126+
_isPresented = isPresented
127+
}
128+
129+
func updateRootView(_ rootView: AnyView) {
130+
hostingController.rootView = rootView
131+
hostingController.view.invalidateIntrinsicContentSize()
132+
hostingController.view.layoutSubtreeIfNeeded()
133+
updateContentSize()
134+
}
135+
136+
func present() {
137+
guard let anchorView, anchorView.window != nil else {
138+
isPresented = false
139+
dismiss()
140+
return
141+
}
142+
143+
anchorView.superview?.layoutSubtreeIfNeeded()
144+
let popover = popover ?? makePopover()
145+
updateContentSize()
146+
guard !popover.isShown else { return }
147+
148+
popover.show(relativeTo: anchorView.bounds, of: anchorView, preferredEdge: .maxY)
149+
}
150+
151+
func dismiss() {
152+
popover?.performClose(nil)
153+
}
154+
155+
func closeFromContent() {
156+
isPresented = false
157+
dismiss()
158+
}
159+
160+
func popoverDidClose(_ notification: Notification) {
161+
popover = nil
162+
if isPresented {
163+
isPresented = false
164+
}
165+
}
166+
167+
private func makePopover() -> NSPopover {
168+
let popover = NSPopover()
169+
popover.behavior = .semitransient
170+
popover.animates = true
171+
popover.contentViewController = hostingController
172+
popover.delegate = self
173+
self.popover = popover
174+
return popover
175+
}
176+
177+
private func updateContentSize() {
178+
let fittingSize = hostingController.view.fittingSize
179+
guard fittingSize.width > 0, fittingSize.height > 0 else { return }
180+
popover?.contentSize = NSSize(
181+
width: ceil(fittingSize.width),
182+
height: ceil(fittingSize.height)
183+
)
184+
}
185+
}
186+
}
187+
78188
/// Menu item that shows "Install Update and Relaunch" when an update is ready.
79189
struct InstallUpdateMenuItem: View {
80190
@ObservedObject var model: UpdateViewModel

0 commit comments

Comments
 (0)