diff options
Diffstat (limited to 'Hotline/Library')
| -rw-r--r-- | Hotline/Library/AsyncLinkPreview.swift | 57 | ||||
| -rw-r--r-- | Hotline/Library/DataAdditions.swift | 23 | ||||
| -rw-r--r-- | Hotline/Library/FileIconView.swift | 74 | ||||
| -rw-r--r-- | Hotline/Library/FileImageView.swift | 98 | ||||
| -rw-r--r-- | Hotline/Library/HotlinePanel.swift | 64 | ||||
| -rw-r--r-- | Hotline/Library/NetSocket.swift | 269 | ||||
| -rw-r--r-- | Hotline/Library/TextView.swift | 119 | ||||
| -rw-r--r-- | Hotline/Library/URLAdditions.swift | 26 | ||||
| -rw-r--r-- | Hotline/Library/VisualEffectView.swift | 22 |
9 files changed, 752 insertions, 0 deletions
diff --git a/Hotline/Library/AsyncLinkPreview.swift b/Hotline/Library/AsyncLinkPreview.swift new file mode 100644 index 0000000..89b186f --- /dev/null +++ b/Hotline/Library/AsyncLinkPreview.swift @@ -0,0 +1,57 @@ +import SwiftUI +import LinkPresentation + +fileprivate class CustomLinkView: LPLinkView { + override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) } +} + +struct AsyncLinkPreview: View { + @State private var metadata: LPLinkMetadata? + @State private var isLoading = true + let url: URL? + + func fetchMetadata() async { + guard let url else { + self.isLoading = false + return + } + do { + let provider = LPMetadataProvider() + let metadata = try await provider.startFetchingMetadata(for: url) + self.metadata = metadata + self.isLoading = false + } catch { + self.isLoading = false + } + } + + var body: some View { + if isLoading { + ProgressView() + .controlSize(.small) + .task { + await self.fetchMetadata() + } + } else if let metadata = metadata { + LinkView(metadata: metadata) + .frame(width: 200) + } else { + Text(LocalizedStringKey(url!.absoluteString)) + .multilineTextAlignment(.leading) + .tint(Color("Link Color")) + } + } +} + +struct LinkView: NSViewRepresentable { + var metadata: LPLinkMetadata + + func makeNSView(context: Context) -> LPLinkView { + let linkView = CustomLinkView(metadata: metadata) + return linkView + } + + func updateNSView(_ nsView: LPLinkView, context: Context) { + // Nothing required + } +} diff --git a/Hotline/Library/DataAdditions.swift b/Hotline/Library/DataAdditions.swift new file mode 100644 index 0000000..0e9dd96 --- /dev/null +++ b/Hotline/Library/DataAdditions.swift @@ -0,0 +1,23 @@ +import Foundation + +extension Data { + func saveAsFileToDownloads(filename: String, bounceDock: Bool = true) -> Bool { + let folderURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0] + let filePath = folderURL.generateUniqueFilePath(filename: filename) + if FileManager.default.createFile(atPath: filePath, contents: nil) { + if let h = FileHandle(forWritingAtPath: filePath) { + try? h.write(contentsOf: self) + try? h.close() + if bounceDock { + #if os(macOS) + var downloadURL = URL(filePath: filePath) + downloadURL.resolveSymlinksInPath() + DistributedNotificationCenter.default().post(name: .init("com.apple.DownloadFileFinished"), object: downloadURL.path) + #endif + } + return true + } + } + return false + } +} diff --git a/Hotline/Library/FileIconView.swift b/Hotline/Library/FileIconView.swift new file mode 100644 index 0000000..d0949fa --- /dev/null +++ b/Hotline/Library/FileIconView.swift @@ -0,0 +1,74 @@ +import SwiftUI +import UniformTypeIdentifiers + +struct FolderIconView: View { + private func folderIcon() -> Image { +#if os(iOS) + return Image(systemName: "folder.fill") +#elseif os(macOS) + return Image(nsImage: NSWorkspace.shared.icon(for: UTType.folder)) +#endif + } + + var body: some View { + folderIcon() + .resizable() + .scaledToFit() + } +} + +struct FileIconView: View { + let filename: String + let fileType: String? + + #if os(iOS) + private func fileIcon() -> Image { + let fileExtension = (self.filename as NSString).pathExtension + if let fileType = UTType(filenameExtension: fileExtension) { + if fileType.isSubtype(of: .movie) { + return Image(systemName: "play.rectangle") + } + else if fileType.isSubtype(of: .image) { + return Image(systemName: "photo") + } + else if fileType.isSubtype(of: .archive) { + return Image(systemName: "doc.zipper") + } + else if fileType.isSubtype(of: .text) { + return Image(systemName: "doc.text") + } + else { + return Image(systemName: "doc") + } + } + + return Image(systemName: "doc") + } + #elseif os(macOS) + private func fileIcon() -> Image { + let fileExtension = (self.filename as NSString).pathExtension + + if !fileExtension.isEmpty, + let uttype = UTType(filenameExtension: fileExtension) { + return Image(nsImage: NSWorkspace.shared.icon(for: uttype)) + } + else if let fileType = self.fileType, + let fileTypeExtension = FileManager.HFSTypeToExtension[fileType.lowercased()], + let uttype = UTType(filenameExtension: fileTypeExtension) { + return Image(nsImage: NSWorkspace.shared.icon(for: uttype)) + } + else { + return Image(nsImage: NSWorkspace.shared.icon(for: UTType.data)) + } + +// Image(nsImage: NSWorkspace.shared.icon(for: UTType(filenameExtension: (filename as NSString).pathExtension) ?? UTType.content)) + } + #endif + + + var body: some View { + fileIcon() + .resizable() + .scaledToFit() + } +} diff --git a/Hotline/Library/FileImageView.swift b/Hotline/Library/FileImageView.swift new file mode 100644 index 0000000..0cc50f1 --- /dev/null +++ b/Hotline/Library/FileImageView.swift @@ -0,0 +1,98 @@ +import SwiftUI + +struct FileImageView: NSViewRepresentable { + var image: NSImage? + + let minimumSize: CGSize = CGSize(width: 350, height: 350) + let presentationPaddingRatio: Double = 0.5 + + func makeNSView(context: Context) -> NSImageView { + let imageView = NSImageView() + imageView.imageScaling = .scaleProportionallyUpOrDown + imageView.animates = true + imageView.isEditable = false + imageView.allowsCutCopyPaste = true + imageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + imageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + if let img = self.image { + imageView.image = img + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.resizeWindowForImageView(imageView) + } + } + + return imageView + } + + func updateNSView(_ nsView: NSImageView, context: Context) { + nsView.image = self.image + } + + // MARK: - + + func resizeWindowForImageView(_ imageView: NSImageView) { + guard let window = imageView.window, let img = imageView.image else { + return + } + + var windowRect = window.contentLayoutRect + let windowChromeSize = CGSize(width: window.frame.width - windowRect.width, height: window.frame.height - windowRect.height) + + var windowMinSize: CGSize = windowRect.size + let centerPoint = CGPoint(x: window.frame.midX, y: window.frame.midY) + + windowRect.size = img.size + + if let screen = window.screen { + var paddedScreenSize = screen.frame.size + paddedScreenSize.width *= self.presentationPaddingRatio + paddedScreenSize.height *= self.presentationPaddingRatio + + windowMinSize = aspectFit(source: windowRect.size, bounds: self.minimumSize) + windowRect.size = aspectFit(source: windowRect.size, bounds: paddedScreenSize, minimum: windowMinSize) + } + + windowRect.size.width += windowChromeSize.width + windowRect.size.height += windowChromeSize.height + + windowRect.origin.x = centerPoint.x - windowRect.width / 2.0 + windowRect.origin.y = centerPoint.y - windowRect.height / 2.0 + + window.setFrame(windowRect, display: true, animate: true) + +// Do these APIs even work?? +// window.aspectRatio = windowRect.size +// window.contentAspectRatio = windowRect.size + } + + func aspectFit(source sourceSize: CGSize, bounds boundingSize: CGSize, minimum minSize: CGSize? = nil) -> CGSize { + let sourceAspectRatio = sourceSize.width / sourceSize.height + + var fitSize: CGSize = sourceSize + + if fitSize.width > boundingSize.width { + fitSize.width = boundingSize.width + fitSize.height = fitSize.width / sourceAspectRatio + } + + if fitSize.height > boundingSize.height { + fitSize.height = boundingSize.height + fitSize.width = fitSize.height * sourceAspectRatio + } + + if let m = minSize { + if fitSize.width < m.width { + fitSize.width = m.width + fitSize.height = fitSize.width / sourceAspectRatio + } + + if fitSize.height < m.height { + fitSize.height = m.height + fitSize.width = fitSize.height * sourceAspectRatio + } + } + + return fitSize + } +} diff --git a/Hotline/Library/HotlinePanel.swift b/Hotline/Library/HotlinePanel.swift new file mode 100644 index 0000000..191e89a --- /dev/null +++ b/Hotline/Library/HotlinePanel.swift @@ -0,0 +1,64 @@ +import Cocoa +import SwiftUI + +fileprivate let HOTLINE_PANEL_SIZE: CGSize = CGSizeMake(468, 114 - 10) + +class HotlinePanel: NSPanel { + init(_ view: HotlinePanelView) { + super.init(contentRect: NSRect(x: 0, y: 0, width: HOTLINE_PANEL_SIZE.width, height: HOTLINE_PANEL_SIZE.height), styleMask: [.nonactivatingPanel, .titled, .closable, .utilityWindow, .fullSizeContentView], backing: .buffered, defer: false) + + // Make sure that the panel is in front of almost all other windows + self.isFloatingPanel = false + self.level = .floating + self.hidesOnDeactivate = true + self.animationBehavior = .utilityWindow + + // Allow the panel to appear in a fullscreen space +// self.collectionBehavior.insert(.fullScreenAuxiliary) + self.collectionBehavior.insert(.canJoinAllSpaces) + self.collectionBehavior.insert(.ignoresCycle) + +// self.appearance = NSAppearance(named: .vibrantDark) + + // Don't delete panel state when it's closed. + self.isReleasedWhenClosed = false + + self.standardWindowButton(.closeButton)?.isHidden = true + self.standardWindowButton(.zoomButton)?.isHidden = true + self.standardWindowButton(.miniaturizeButton)?.isHidden = true + + // Make it transparent, the view inside will have to set the background. + // This is necessary because otherwise, we will have some space for the titlebar on top of the height of the view itself which we don't want. + self.isOpaque = false + self.backgroundColor = .clear + + // Since we don't show a statusbar, this allows us to drag the window by its background instead of the titlebar. + self.isMovableByWindowBackground = true + self.titlebarAppearsTransparent = true + + let hostingView = NSHostingView(rootView: view.edgesIgnoringSafeArea(.top)) + hostingView.sizingOptions = [.preferredContentSize] + + let visualEffectView = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: HOTLINE_PANEL_SIZE.width, height: HOTLINE_PANEL_SIZE.height)) + visualEffectView.material = .sidebar + visualEffectView.blendingMode = .behindWindow + visualEffectView.state = NSVisualEffectView.State.active + visualEffectView.autoresizingMask = [.width, .height] + visualEffectView.autoresizesSubviews = true + visualEffectView.addSubview(hostingView) + + self.contentView = visualEffectView + + hostingView.frame = visualEffectView.bounds + + self.cascadeTopLeft(from: NSMakePoint(16, 16)) + } + + override var canBecomeKey: Bool { + return false + } + + override var canBecomeMain: Bool { + return false + } +} diff --git a/Hotline/Library/NetSocket.swift b/Hotline/Library/NetSocket.swift new file mode 100644 index 0000000..a7d44ce --- /dev/null +++ b/Hotline/Library/NetSocket.swift @@ -0,0 +1,269 @@ + +// NetSocket.swift +// A simple delegate based buffered read/write TCP socket. +// Created by Dustin Mierau + +import Foundation + +protocol NetSocketDelegate: AnyObject { + func netsocketConnected(socket: NetSocket) + func netsocketDisconnected(socket: NetSocket, error: Error?) + func netsocketReceived(socket: NetSocket, bytes: [UInt8]) + func netsocketSent(socket: NetSocket, count: Int) +} + +extension NetSocketDelegate { + func netsocketConnected(socket: NetSocket) {} + func netsocketDisconnected(socket: NetSocket, error: Error?) {} + func netsocketReceived(socket: NetSocket, bytes: [UInt8]) {} + func netsocketSent(socket: NetSocket, count: Int) {} +} + +enum NetSocketStatus { + case disconnected + case connecting + case connected +} + +final class NetSocket: NSObject, StreamDelegate { + weak var delegate: NetSocketDelegate? = nil + + private var output: OutputStream? = nil + private var input: InputStream? = nil + + private var outputBuffer: [UInt8] = [] + private var inputBuffer: [UInt8] = [] + + private var readBuffer: [UInt8] = Array(repeating: 0, count: 4 * 1024) + + public func peek() -> [UInt8] { self.inputBuffer } + public var available: Int { self.inputBuffer.count } + + private var status: NetSocketStatus = .disconnected + + @MainActor public func has(_ length: Int) -> Bool { + return (self.available >= length) + } + + override init() {} + + @MainActor public func connect(host: String, port: Int) { + self.close() + + var outputStream: OutputStream? = nil + var inputStream: InputStream? = nil + + self.status = .connecting + + Stream.getStreamsToHost(withName: host, port: port, inputStream: &inputStream, outputStream: &outputStream) + + self.input = inputStream + self.output = outputStream + + inputStream?.delegate = self + outputStream?.delegate = self + + inputStream?.schedule(in: .current, forMode: .default) + outputStream?.schedule(in: .current, forMode: .default) + + inputStream?.open() + outputStream?.open() + } + + @MainActor public func close(_ err: Error? = nil) { + print("NetSocket: Closed") + + let disconnected = (self.status != .disconnected) + + self.status = .disconnected + + self.input?.delegate = nil + self.output?.delegate = nil + self.input?.close() + self.output?.close() + self.input?.remove(from: .current, forMode: .default) + self.output?.remove(from: .current, forMode: .default) + self.input = nil + self.output = nil + self.inputBuffer = [] + self.outputBuffer = [] + + if disconnected { + self.delegate?.netsocketDisconnected(socket: self, error: err) + } + } + + @MainActor public func write(_ data: Data) { + guard let output = self.output else { + return + } + + self.outputBuffer.append(contentsOf: data) + + if output.hasSpaceAvailable { + self.writeBufferToStream() + } + } + + @MainActor public func write(_ data: [UInt8]) { + guard let output = self.output else { + return + } + + self.outputBuffer.append(contentsOf: data) + + if output.hasSpaceAvailable { + self.writeBufferToStream() + } + } + + @MainActor public func read(count: Int) -> [UInt8] { + guard self.inputBuffer.count > 0, count > 0 else { + return [] + } + + let amountToRead = min(count, self.inputBuffer.count) + let dataRead: [UInt8] = Array(self.inputBuffer[0..<amountToRead]) + self.inputBuffer.removeFirst(amountToRead) + + return dataRead + } + + @MainActor public func read(count: Int) -> Data { + guard self.inputBuffer.count > 0, count > 0 else { + return Data() + } + + let amountToRead = min(count, self.inputBuffer.count) + + let dataRead: Data = Data(self.inputBuffer[0..<amountToRead]) + self.inputBuffer.removeFirst(amountToRead) + + return dataRead + } + + @MainActor public func readAll() -> [UInt8] { + guard self.inputBuffer.count > 0 else { + return [] + } + + let dataRead: [UInt8] = Array(self.inputBuffer) + self.inputBuffer = [] + + return dataRead + } + + @MainActor public func readAll() -> Data { + guard self.inputBuffer.count > 0 else { + return Data() + } + + let dataRead: Data = Data(self.inputBuffer) + self.inputBuffer = [] + + return dataRead + } + + @MainActor private func writeBufferToStream() { + guard let output = self.output, self.outputBuffer.count > 0 else { + return + } + + let bytesWritten = output.write(self.outputBuffer, maxLength: self.outputBuffer.count) + print("NetSocket => \(bytesWritten) bytes") + if bytesWritten > 0 { + self.outputBuffer.removeFirst(bytesWritten) + self.delegate?.netsocketSent(socket: self, count: bytesWritten) + } + else if bytesWritten == -1 { + self.close(output.streamError) + } + } + + @MainActor private func readStreamToBuffer() { + guard let input = self.input else { + return + } + + let bytesRead = input.read(&self.readBuffer, maxLength: 4 * 1024) + print("NetSocket <= \(bytesRead) bytes") + if bytesRead > 0 { + self.inputBuffer.append(contentsOf: self.readBuffer[0..<bytesRead]) + self.delegate?.netsocketReceived(socket: self, bytes: self.inputBuffer) + } + else if bytesRead == -1 { + self.close(input.streamError) + } + } + + // MARK: - + + @MainActor func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + guard let input = self.input, let output = self.output else { + return + } + + switch eventCode { + case .openCompleted: + if aStream == input { + self.setupStreamOptions() + } + if input.streamStatus == .open && output.streamStatus == .open { + if self.status == .connecting { + print("NetSocket: Connected") + self.status = .connected + self.delegate?.netsocketConnected(socket: self) + } + } + break + case .hasBytesAvailable: + print("NetSocket: Data available") + self.readStreamToBuffer() + case .hasSpaceAvailable: + print("NetSocket: Space available") + self.writeBufferToStream() + case .endEncountered: + print("NetSocket: End encounterd") + self.close() + case .errorOccurred: + print("NetSocket: Error encounterd", input.streamError.debugDescription, output.streamError.debugDescription) + let err = input.streamError ?? output.streamError + self.close(err) + default: + break + } + } + + // MARK: - + + private func setupStreamOptions() { + if let input = self.input { + let socketData: Data = CFReadStreamCopyProperty(input as CFReadStream, CFStreamPropertyKey.socketNativeHandle) as! Data; + var socketHandle: CFSocketNativeHandle = 0; + (socketData as NSData).getBytes(&socketHandle, length: MemoryLayout.size(ofValue: socketHandle)); + + var value: Int = 0; + let size = UInt32(MemoryLayout.size(ofValue: value)); + + value = 1; + if setsockopt(socketHandle, IPPROTO_TCP, TCP_NODELAY, &value, size) != 0 { + print("NetSocket: failed to set TCP_NODELAY"); + } + // Enable keepalive + value = 1; + if setsockopt(socketHandle, SOL_SOCKET, SO_KEEPALIVE, &value, size) != 0 { + print("NetSocket: failed to set SO_KEEPALIVE"); + } +// // Number of keepalives before close (including first keepalive packet) +// value = 5 +// if setsockopt(socketHandle, IPPROTO_TCP, TCP_KEEPCNT, &value, size) != 0 { +// print("NetSocket: failed to set TCP_KEEPCNT"); +// } +// // Idle time used when SO_KEEPALIVE is enabled. Sets how long connection must be idle before keepalive is sent. +// value = 60 +// if setsockopt(socketHandle, IPPROTO_TCP, TCP_KEEPALIVE, &value, size) != 0 { +// print("NetSocket: failed to set TCP_KEEPALIVE") +// } + } + } +} diff --git a/Hotline/Library/TextView.swift b/Hotline/Library/TextView.swift new file mode 100644 index 0000000..47746e2 --- /dev/null +++ b/Hotline/Library/TextView.swift @@ -0,0 +1,119 @@ +import SwiftUI + +struct BetterTextEditor: NSViewRepresentable { + + @Environment(\.lineSpacing) private var lineSpacing + + @Binding private var text: String + + private var customizations: [(NSTextView) -> Void] = [] + + init(text: Binding<String>) { + self._text = text + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeNSView(context: Context) -> NSScrollView { + let scrollview = NSTextView.scrollablePlainDocumentContentTextView() + let textview = scrollview.documentView as! NSTextView + + textview.string = self.text + textview.delegate = context.coordinator + +// let p = NSMutableParagraphStyle() +// p.lineSpacing = self.lineSpacing +//// textview.defaultParagraphStyle = p +// textview.typingAttributes = [ +// .paragraphStyle: p +// ] + + textview.isEditable = true + textview.isRichText = false + textview.allowsUndo = true + textview.isFieldEditor = false + textview.usesAdaptiveColorMappingForDarkAppearance = true + textview.drawsBackground = false // true + textview.usesRuler = false + textview.usesFindBar = false + textview.isIncrementalSearchingEnabled = false + textview.isAutomaticQuoteSubstitutionEnabled = false + textview.isAutomaticDashSubstitutionEnabled = false + textview.isAutomaticSpellingCorrectionEnabled = true + textview.isAutomaticDataDetectionEnabled = false + textview.isAutomaticLinkDetectionEnabled = false + textview.usesInspectorBar = false + textview.usesFontPanel = false + textview.importsGraphics = false + textview.allowsImageEditing = false + textview.displaysLinkToolTips = true + textview.backgroundColor = NSColor.textBackgroundColor + textview.textContainerInset = NSSize(width: 16, height: 16) + textview.isContinuousSpellCheckingEnabled = true + textview.setSelectedRange(NSMakeRange(0, 0)) + self.customizations.forEach { $0(textview) } + + scrollview.scrollerStyle = .overlay + + return scrollview + } + + func updateNSView(_ nsView: NSScrollView, context: Context) { + let textview = nsView.documentView as! NSTextView + + if textview.string != text { + textview.string = text + } + + self.customizations.forEach { $0(textview) } + } + + func betterEditorFont(_ font: NSFont) -> Self { + self.customized { $0.font = font } + } + + func betterEditorParagraphStyle(_ paragraphStyle: NSParagraphStyle) -> Self { + self.customized { $0.defaultParagraphStyle = paragraphStyle } + } + + func betterEditorAutomaticDashSubstitution(_ enabled: Bool) -> Self { + self.customized { $0.isAutomaticDashSubstitutionEnabled = enabled } + } + + func betterEditorAutomaticQuoteSubstitution(_ enabled: Bool) -> Self { + self.customized { $0.isAutomaticQuoteSubstitutionEnabled = enabled } + } + + func betterEditorAutomaticSpellingCorrection(_ enabled: Bool) -> Self { + self.customized { $0.isAutomaticSpellingCorrectionEnabled = enabled } + } + + func betterEditorTextInset(_ size: NSSize) -> Self { + self.customized { $0.textContainerInset = size } + } + + class Coordinator: NSObject, NSTextViewDelegate { + var parent: BetterTextEditor + + init(_ parent: BetterTextEditor) { + self.parent = parent + } + + func textDidChange(_ notification: Notification) { + guard let textview = notification.object as? NSTextView else { + return + } + self.parent.text = textview.string + } + } +} + +private extension BetterTextEditor { + private func customized(_ customization: @escaping (NSTextView) -> Void) -> Self { + var copy = self + copy.customizations.append(customization) + return copy + } +} diff --git a/Hotline/Library/URLAdditions.swift b/Hotline/Library/URLAdditions.swift new file mode 100644 index 0000000..1f25541 --- /dev/null +++ b/Hotline/Library/URLAdditions.swift @@ -0,0 +1,26 @@ +import Foundation + +extension URL { + func generateUniqueFilePath(filename base: String) -> String { + let fileManager = FileManager.default + var finalName = base + var counter = 2 + + // Helper function to generate a new filename with a counter + func makeFileName() -> String { + let baseName = (base as NSString).deletingPathExtension + let extensionName = (base as NSString).pathExtension + return extensionName.isEmpty ? "\(baseName) \(counter)" : "\(baseName) \(counter).\(extensionName)" + } + + // Check if file exists and append counter until a unique name is found + var filePath = self.appending(component: finalName).path(percentEncoded: false) + while fileManager.fileExists(atPath: filePath) { + finalName = makeFileName() + filePath = self.appending(component: finalName).path(percentEncoded: false) + counter += 1 + } + + return filePath + } +} diff --git a/Hotline/Library/VisualEffectView.swift b/Hotline/Library/VisualEffectView.swift new file mode 100644 index 0000000..e595c55 --- /dev/null +++ b/Hotline/Library/VisualEffectView.swift @@ -0,0 +1,22 @@ +import SwiftUI + +struct VisualEffectView: NSViewRepresentable +{ + let material: NSVisualEffectView.Material + let blendingMode: NSVisualEffectView.BlendingMode + + func makeNSView(context: Context) -> NSVisualEffectView + { + let visualEffectView = NSVisualEffectView() + visualEffectView.material = material + visualEffectView.blendingMode = blendingMode + visualEffectView.state = NSVisualEffectView.State.active + return visualEffectView + } + + func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) + { + visualEffectView.material = material + visualEffectView.blendingMode = blendingMode + } +} |