{"id":3247,"date":"2026-04-09T14:24:55","date_gmt":"2026-04-09T13:24:55","guid":{"rendered":"https:\/\/www.kangama.com\/?p=3247"},"modified":"2026-05-07T14:16:24","modified_gmt":"2026-05-07T13:16:24","slug":"floating-panel-macos-swiftui-nspanel","status":"publish","type":"post","link":"https:\/\/www.kangama.com\/en\/floating-panel-macos-swiftui-nspanel\/","title":{"rendered":"Floating Panel sur macOS avec SwiftUI : ouvrir une fen\u00eatre avanc\u00e9e depuis la menu bar"},"content":{"rendered":"<div id=\"bsf_rt_marker\"><\/div>\n<h2 class=\"wp-block-heading\">Introduction<\/h2>\n\n\n\n<p>Lorsque l\u2019on d\u00e9veloppe une application macOS bas\u00e9e sur une <strong>menu bar<\/strong>, une limite appara\u00eet rapidement : comment afficher une interface plus riche sans casser l\u2019exp\u00e9rience utilisateur ?<\/p>\n\n\n\n<p>Dans mon cas, apr\u00e8s avoir mis en place une menu bar pour mon <a href=\"https:\/\/www.kangama.com\/outil-macos-nettoyer-cache-developpeur\/\" data-type=\"post\" data-id=\"3172\"><strong>outil de nettoyage de caches d\u00e9veloppeur<\/strong><\/a>, j\u2019ai eu besoin d\u2019aller plus loin. L\u2019objectif \u00e9tait simple en apparence : afficher une vue d\u00e9taill\u00e9e des caches lorsqu\u2019un utilisateur clique sur un \u00e9l\u00e9ment, tout en gardant une interaction fluide, l\u00e9g\u00e8re et coh\u00e9rente avec macOS.<\/p>\n\n\n\n<p>C\u2019est l\u00e0 qu\u2019intervient le concept de <strong>floating panel avec NSPanel<\/strong>.<\/p>\n\n\n\n<p>Dans cet article, nous allons voir comment impl\u00e9menter un <strong>NSPanel avec SwiftUI<\/strong>, pourquoi ce choix est souvent plus pertinent qu\u2019une fen\u00eatre classique, et comment r\u00e9soudre deux probl\u00e9matiques concr\u00e8tes :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">fermer automatiquement le panel lorsque la vue principale dispara\u00eet<\/li>\n\n\n\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">positionner dynamiquement le panel \u00e0 c\u00f4t\u00e9 de la fen\u00eatre principale<\/li>\n<\/ul>\n\n\n\n<p>Le tout avec une approche pragmatique issue d\u2019un projet r\u00e9el.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Pourquoi utiliser un floating panel plut\u00f4t qu\u2019une fen\u00eatre classique ?<\/h2>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\">NSWindow vs NSPanel : une diff\u00e9rence cl\u00e9<\/h3>\n\n\n\n<p>Sur macOS, ouvrir une nouvelle interface peut se faire de plusieurs mani\u00e8res. La plus classique reste l\u2019utilisation d\u2019un <strong>NSWindow<\/strong>, mais ce n\u2019est pas toujours la meilleure option.<\/p>\n\n\n\n<p>Un <strong>NSPanel<\/strong> est en r\u00e9alit\u00e9 une sous-classe de <code>NSWindow<\/code>, mais avec un comportement sp\u00e9cifique con\u00e7u pour des interfaces secondaires, comme :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">des panneaux flottants<\/li>\n\n\n\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">des inspecteurs<\/li>\n\n\n\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">des fen\u00eatres contextuelles non bloquantes<\/li>\n<\/ul>\n\n\n\n<p>Contrairement \u00e0 une fen\u00eatre classique, un <strong>NSPanel<\/strong> permet :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">de rester au-dessus des autres fen\u00eatres<\/li>\n\n\n\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">de ne pas forc\u00e9ment appara\u00eetre dans le dock<\/li>\n\n\n\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">de se comporter comme un \u00e9l\u00e9ment temporaire de l\u2019interface<\/li>\n\n\n\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">de mieux g\u00e9rer le focus utilisateur<\/li>\n<\/ul>\n\n\n\n<p>Dans le cadre d\u2019une application en menu bar, ces caract\u00e9ristiques sont essentielles.<\/p>\n\n\n\n<p>Une fen\u00eatre classique aurait donn\u00e9 une sensation de rupture dans l\u2019exp\u00e9rience. \u00c0 l\u2019inverse, le <strong>floating panel s\u2019int\u00e8gre naturellement<\/strong>, comme une extension de la vue principale.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Cas concret : afficher une vue de d\u00e9tail des caches<\/h2>\n\n\n\n<p>Dans mon application, chaque cat\u00e9gorie de cache peut \u00eatre explor\u00e9e plus en profondeur. Lorsqu\u2019un utilisateur clique sur un \u00e9l\u00e9ment, un panneau vient s\u2019ouvrir \u00e0 c\u00f4t\u00e9 de la vue principale pour afficher les chemins d\u00e9taill\u00e9s.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img fetchpriority=\"high\" decoding=\"async\" width=\"1056\" height=\"630\" src=\"https:\/\/www.kangama.com\/wp-content\/uploads\/2026\/04\/DevCacheCleaner-NSPanel-detail.png\" alt=\"Floating Panel\" class=\"wp-image-3252\" srcset=\"https:\/\/www.kangama.com\/wp-content\/uploads\/2026\/04\/DevCacheCleaner-NSPanel-detail.png 1056w, https:\/\/www.kangama.com\/wp-content\/uploads\/2026\/04\/DevCacheCleaner-NSPanel-detail-744x444.png 744w, https:\/\/www.kangama.com\/wp-content\/uploads\/2026\/04\/DevCacheCleaner-NSPanel-detail-420x251.png 420w, https:\/\/www.kangama.com\/wp-content\/uploads\/2026\/04\/DevCacheCleaner-NSPanel-detail-768x458.png 768w\" sizes=\"(max-width: 1056px) 100vw, 1056px\" \/><\/figure>\n\n\n\n<p>Cette approche permet de conserver le contexte tout en apportant un niveau de d\u00e9tail suppl\u00e9mentaire. L\u2019utilisateur n\u2019a pas besoin de naviguer entre plusieurs fen\u00eatres. l\u2019information appara\u00eet imm\u00e9diatement, au bon endroit.<\/p>\n\n\n\n<p>Ce type d\u2019interaction est particuli\u00e8rement adapt\u00e9 aux outils d\u00e9veloppeur, o\u00f9 la lisibilit\u00e9 et la rapidit\u00e9 d\u2019acc\u00e8s sont primordiales.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Une base solide : l\u2019approche propos\u00e9e par l&rsquo;article du site Cindori<\/h2>\n\n\n\n<p>SwiftUI ne propose pas encore de solution native compl\u00e8te pour g\u00e9rer les <code>NSPanel<\/code>. Il est donc n\u00e9cessaire de s\u2019appuyer sur AppKit.<\/p>\n\n\n\n<p>Pour cela, je me suis bas\u00e9 sur un article de r\u00e9f\u00e9rence propos\u00e9 par le site <strong><a href=\"http:\/\/Pour cela, je me suis appuy\u00e9 sur cet excellent article : https:\/\/cindori.com\/developer\/floating-panel\">Cindori<\/a><\/strong>, qui pr\u00e9sente une approche propre et structur\u00e9e pour int\u00e9grer un floating panel dans une application SwiftUI.<\/p>\n\n\n<div class=\"vlp-link-container vlp-layout-basic wp-block-visual-link-preview-link\"><a href=\"https:\/\/cindori.com\/developer\/floating-panel\" class=\"vlp-link\" title=\"Make a floating panel in SwiftUI for macOS\" rel=\"nofollow noopener\" target=\"_blank\"><\/a><div class=\"vlp-layout-zone-side\"><div class=\"vlp-block-2 vlp-link-image\"><img decoding=\"async\" src=\"https:\/\/cindori.com\/images\/blog\/blog-social-card.png\" style=\"max-width: 150px; max-height: 150px\" \/><\/div><\/div><div class=\"vlp-layout-zone-main\"><div class=\"vlp-block-0 vlp-link-title\">Make a floating panel in SwiftUI for macOS<\/div><div class=\"vlp-block-1 vlp-link-summary\">Learn how to create versatile floating panels for macOS using SwiftUI and AppKit, enhancing your app with dynamic and customizable designs.<\/div><\/div><\/div>\n\n\n<p>Ce qui rend cette approche int\u00e9ressante, ce n\u2019est pas uniquement le code, mais surtout la mani\u00e8re dont elle organise les responsabilit\u00e9s :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">une sous-classe de <code>NSPanel<\/code><\/li>\n\n\n\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">une int\u00e9gration via <code>NSHostingView<\/code><\/li>\n\n\n\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">un <code>Binding<\/code> pour piloter l\u2019\u00e9tat<\/li>\n\n\n\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\">un <code>ViewModifier<\/code> pour exposer le comportement c\u00f4t\u00e9 SwiftUI<\/li>\n<\/ul>\n\n\n\n<p>Cette base permet de construire une impl\u00e9mentation claire, \u00e9volutive, et surtout r\u00e9utilisable.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Impl\u00e9mentation : cr\u00e9er un FloatingPanel avec SwiftUI<\/h2>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\">Cr\u00e9er une sous-classe de NSPanel<\/h3>\n\n\n\n<p>La premi\u00e8re \u00e9tape consiste \u00e0 encapsuler la logique dans une classe d\u00e9di\u00e9e.<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#f6f6f4;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>\nimport SwiftUI\nimport AppKit\n\nfinal class FloatingPanel&lt;Content: View>: NSPanel {\n\n    @Binding private var isPresented: Bool\n\n    init(\n        contentRect: NSRect,\n        isPresented: Binding&lt;Bool>,\n        @ViewBuilder content: () -> Content\n    ) {\n        self._isPresented = isPresented\n\n        super.init(\n            contentRect: contentRect,\n            styleMask: &#91;.nonactivatingPanel, .titled, .fullSizeContentView&#93;,\n            backing: .buffered,\n            defer: false\n        )\n\n        \/\/\/ Autoriser le placement du panneau au-dessus des autres fen\u00eatres\n        isFloatingPanel = true\n        level = .floating\n     \n        \/\/\/ Autoriser la superposition du panneau dans un espace plein \u00e9cran\n        collectionBehavior.insert(.fullScreenAuxiliary)\n     \n        \/\/\/ Ne pas afficher le titre de la fen\u00eatre, m\u00eame s'il est d\u00e9fini.\n        titleVisibility = .hidden\n        titlebarAppearsTransparent = true\n        backgroundColor = .clear\n        isOpaque = false\n        \n        \/\/ Ne pas autoriser le d\u00e9placement du panneau\n        isMovable = false\n        isReleasedWhenClosed = false\n     \n        \/\/\/ Masquer tous les boutons de la fen\u00eatre\n        standardWindowButton(.closeButton)?.isHidden = true\n        standardWindowButton(.miniaturizeButton)?.isHidden = true\n        standardWindowButton(.zoomButton)?.isHidden = true\n     \n        \/\/\/ Configure les animations en cons\u00e9quence\n        animationBehavior = .utilityWindow\n\n        contentView = NSHostingView(rootView: content())\n    }\n\n    override func close() {\n        super.close()\n        isPresented = false\n    }\n    \n    \/\/\/ Les propri\u00e9t\u00e9s `canBecomeKey` et `canBecomeMain` sont toutes deux requises \n    \/\/\/ pour que les champs de texte \u00e0 l'int\u00e9rieur du panneau puissent recevoir le focus.\n    override var canBecomeKey: Bool { true }\n    override var canBecomeMain: Bool { true }\n\n}\n<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki dracula-soft\" style=\"background-color: #282A36\" tabindex=\"0\"><code><span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F286C4\">import<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1; font-style: italic\">SwiftUI<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F286C4\">import<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1; font-style: italic\">AppKit<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F286C4\">final<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">class<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1\">FloatingPanel<\/span><span style=\"color: #F6F6F4\">&lt;<\/span><span style=\"color: #BF9EEE; font-style: italic\">Content<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #97E1F1; font-style: italic\">View<\/span><span style=\"color: #F6F6F4\">&gt;: <\/span><span style=\"color: #97E1F1; font-style: italic\">NSPanel <\/span><span style=\"color: #F6F6F4\">{<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">@Binding<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">private<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">var<\/span><span style=\"color: #F6F6F4\"> isPresented: <\/span><span style=\"color: #97E1F1; font-style: italic\">Bool<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">init<\/span><span style=\"color: #F6F6F4\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #62E884; font-style: italic\">contentRect<\/span><span style=\"color: #F6F6F4\">: NSRect,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #62E884; font-style: italic\">isPresented<\/span><span style=\"color: #F6F6F4\">: Binding&lt;<\/span><span style=\"color: #97E1F1; font-style: italic\">Bool<\/span><span style=\"color: #F6F6F4\">&gt;,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        @<\/span><span style=\"color: #62E884\">ViewBuilder<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #FFB86C; font-style: italic\">content<\/span><span style=\"color: #F6F6F4\">: () <\/span><span style=\"color: #F286C4\">-&gt;<\/span><span style=\"color: #F6F6F4\"> Content<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    ) {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #BF9EEE; font-style: italic\">self<\/span><span style=\"color: #F6F6F4\">._isPresented <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> isPresented<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #BF9EEE; font-style: italic\">super<\/span><span style=\"color: #F6F6F4\">.<\/span><span style=\"color: #F286C4\">init<\/span><span style=\"color: #F6F6F4\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            <\/span><span style=\"color: #97E1F1\">contentRect<\/span><span style=\"color: #F6F6F4\">: contentRect,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            <\/span><span style=\"color: #97E1F1\">styleMask<\/span><span style=\"color: #F6F6F4\">: &#91;.nonactivatingPanel, .titled, .fullSizeContentView&#93;,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            <\/span><span style=\"color: #97E1F1\">backing<\/span><span style=\"color: #F6F6F4\">: .buffered,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            <\/span><span style=\"color: #97E1F1\">defer<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #BF9EEE\">false<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        )<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #7B7F8B\">\/\/\/ Autoriser le placement du panneau au-dessus des autres fen\u00eatres<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        isFloatingPanel <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">true<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        level <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> .floating<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">     <\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #7B7F8B\">\/\/\/ Autoriser la superposition du panneau dans un espace plein \u00e9cran<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        collectionBehavior.<\/span><span style=\"color: #97E1F1\">insert<\/span><span style=\"color: #F6F6F4\">(.fullScreenAuxiliary)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">     <\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #7B7F8B\">\/\/\/ Ne pas afficher le titre de la fen\u00eatre, m\u00eame s&#39;il est d\u00e9fini.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        titleVisibility <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> .hidden<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        titlebarAppearsTransparent <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">true<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        backgroundColor <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> .clear<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        isOpaque <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">false<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #7B7F8B\">\/\/ Ne pas autoriser le d\u00e9placement du panneau<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        isMovable <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">false<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        isReleasedWhenClosed <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">false<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">     <\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #7B7F8B\">\/\/\/ Masquer tous les boutons de la fen\u00eatre<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #97E1F1\">standardWindowButton<\/span><span style=\"color: #F6F6F4\">(.closeButton)<\/span><span style=\"color: #F286C4\">?<\/span><span style=\"color: #F6F6F4\">.isHidden <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">true<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #97E1F1\">standardWindowButton<\/span><span style=\"color: #F6F6F4\">(.miniaturizeButton)<\/span><span style=\"color: #F286C4\">?<\/span><span style=\"color: #F6F6F4\">.isHidden <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">true<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #97E1F1\">standardWindowButton<\/span><span style=\"color: #F6F6F4\">(.zoomButton)<\/span><span style=\"color: #F286C4\">?<\/span><span style=\"color: #F6F6F4\">.isHidden <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">true<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">     <\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #7B7F8B\">\/\/\/ Configure les animations en cons\u00e9quence<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        animationBehavior <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> .utilityWindow<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        contentView <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1\">NSHostingView<\/span><span style=\"color: #F6F6F4\">(<\/span><span style=\"color: #97E1F1\">rootView<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #97E1F1\">content<\/span><span style=\"color: #F6F6F4\">())<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">override<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">func<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #62E884\">close<\/span><span style=\"color: #F6F6F4\">() {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #BF9EEE; font-style: italic\">super<\/span><span style=\"color: #F6F6F4\">.<\/span><span style=\"color: #97E1F1\">close<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        isPresented <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">false<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/\/ Les propri\u00e9t\u00e9s `canBecomeKey` et `canBecomeMain` sont toutes deux requises <\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/\/ pour que les champs de texte \u00e0 l&#39;int\u00e9rieur du panneau puissent recevoir le focus.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">override<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">var<\/span><span style=\"color: #F6F6F4\"> canBecomeKey: <\/span><span style=\"color: #97E1F1; font-style: italic\">Bool<\/span><span style=\"color: #F6F6F4\"> { <\/span><span style=\"color: #BF9EEE\">true<\/span><span style=\"color: #F6F6F4\"> }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">override<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">var<\/span><span style=\"color: #F6F6F4\"> canBecomeMain: <\/span><span style=\"color: #97E1F1; font-style: italic\">Bool<\/span><span style=\"color: #F6F6F4\"> { <\/span><span style=\"color: #BF9EEE\">true<\/span><span style=\"color: #F6F6F4\"> }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">}<\/span><\/span>\n<span class=\"line\"><\/span><\/code><\/pre><\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<p>Cette impl\u00e9mentation permet d\u2019int\u00e9grer une vue SwiftUI dans un <code>NSPanel<\/code>, tout en gardant un contr\u00f4le sur son \u00e9tat d\u2019affichage.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Int\u00e9gration c\u00f4t\u00e9 SwiftUI<\/h3>\n\n\n\n<p>Pour rendre l\u2019utilisation plus naturelle, j\u2019ai encapsul\u00e9 l\u2019ouverture du panel dans un <code>ViewModifier<\/code>.<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#f6f6f4;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>import SwiftUI\nimport AppKit\n\nstruct FloatingPanelModifier&lt;PanelContent: View>: ViewModifier {\n\n    \/\/\/ D\u00e9termine si le panel doit \u00eatre pr\u00e9sent\u00e9 ou non\n    @Binding var isPresented: Bool\n\n    \/\/\/ Contient la vue du contenu du panneau\n    let content: () -> PanelContent\n\n    \/\/\/ Stocke l'instance du panneau\n    @State private var panel: FloatingPanel&lt;PanelContent>?\n\n    func body(content view: Content) -> some View {\n        view\n            .onAppear {\n                ensurePanel()\n            }\n            .onChange(of: isPresented) { _, newValue in\n                if newValue {\n                    present()\n                } else {\n                   panel?.close()\n                }\n            }\n    }\n    \n    \/\/\/ Pr\u00e9sentez le panneau et en faire la fen\u00eatre principale\n    func present() {\n        panel?.orderFront(nil)\n        panel?.makeKey()\n    }\n    \n    \/\/\/ Lorsque la vue appara\u00eet, cr\u00e9ez et pr\u00e9sentez le panneau si n\u00e9cessaire.\n    func ensurePanel() {\n        if panel == nil {\n            panel = FloatingPanel(\n                contentRect: NSRect(x: 0, y: 0, width: 400, height: 500),\n                isPresented: $isPresented,\n                content: content\n            )\n        }\n    }\n\n}<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki dracula-soft\" style=\"background-color: #282A36\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #F286C4\">import<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1; font-style: italic\">SwiftUI<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F286C4\">import<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1; font-style: italic\">AppKit<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F286C4\">struct<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1; font-style: italic\">FloatingPanelModifier<\/span><span style=\"color: #F6F6F4\">&lt;<\/span><span style=\"color: #BF9EEE; font-style: italic\">PanelContent<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #97E1F1; font-style: italic\">View<\/span><span style=\"color: #F6F6F4\">&gt;: <\/span><span style=\"color: #97E1F1; font-style: italic\">ViewModifier <\/span><span style=\"color: #F6F6F4\">{<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/\/ D\u00e9termine si le panel doit \u00eatre pr\u00e9sent\u00e9 ou non<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">@Binding<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">var<\/span><span style=\"color: #F6F6F4\"> isPresented: <\/span><span style=\"color: #97E1F1; font-style: italic\">Bool<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/\/ Contient la vue du contenu du panneau<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">let<\/span><span style=\"color: #F6F6F4\"> content: () <\/span><span style=\"color: #F286C4\">-&gt;<\/span><span style=\"color: #F6F6F4\"> PanelContent<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/\/ Stocke l&#39;instance du panneau<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">@State<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">private<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">var<\/span><span style=\"color: #F6F6F4\"> panel: FloatingPanel&lt;PanelContent&gt;<\/span><span style=\"color: #F286C4\">?<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">func<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #62E884\">body<\/span><span style=\"color: #F6F6F4\">(<\/span><span style=\"color: #62E884\">content<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #FFB86C; font-style: italic\">view<\/span><span style=\"color: #F6F6F4\">: Content) <\/span><span style=\"color: #F286C4\">-&gt;<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">some<\/span><span style=\"color: #F6F6F4\"> View {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        view<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            .<\/span><span style=\"color: #97E1F1\">onAppear<\/span><span style=\"color: #F6F6F4\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">                <\/span><span style=\"color: #97E1F1\">ensurePanel<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            .<\/span><span style=\"color: #97E1F1\">onChange<\/span><span style=\"color: #F6F6F4\">(<\/span><span style=\"color: #97E1F1\">of<\/span><span style=\"color: #F6F6F4\">: isPresented) { <\/span><span style=\"color: #BF9EEE\">_<\/span><span style=\"color: #F6F6F4\">, newValue <\/span><span style=\"color: #F286C4\">in<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">                <\/span><span style=\"color: #F286C4\">if<\/span><span style=\"color: #F6F6F4\"> newValue {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">                    <\/span><span style=\"color: #97E1F1\">present<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">                } <\/span><span style=\"color: #F286C4\">else<\/span><span style=\"color: #F6F6F4\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">                   panel<\/span><span style=\"color: #F286C4\">?<\/span><span style=\"color: #F6F6F4\">.<\/span><span style=\"color: #97E1F1\">close<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">                }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/\/ Pr\u00e9sentez le panneau et en faire la fen\u00eatre principale<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">func<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #62E884\">present<\/span><span style=\"color: #F6F6F4\">() {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        panel<\/span><span style=\"color: #F286C4\">?<\/span><span style=\"color: #F6F6F4\">.<\/span><span style=\"color: #97E1F1\">orderFront<\/span><span style=\"color: #F6F6F4\">(<\/span><span style=\"color: #BF9EEE\">nil<\/span><span style=\"color: #F6F6F4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        panel<\/span><span style=\"color: #F286C4\">?<\/span><span style=\"color: #F6F6F4\">.<\/span><span style=\"color: #97E1F1\">makeKey<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/\/ Lorsque la vue appara\u00eet, cr\u00e9ez et pr\u00e9sentez le panneau si n\u00e9cessaire.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">func<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #62E884\">ensurePanel<\/span><span style=\"color: #F6F6F4\">() {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #F286C4\">if<\/span><span style=\"color: #F6F6F4\"> panel <\/span><span style=\"color: #F286C4\">==<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">nil<\/span><span style=\"color: #F6F6F4\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            panel <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1\">FloatingPanel<\/span><span style=\"color: #F6F6F4\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">                <\/span><span style=\"color: #97E1F1\">contentRect<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #97E1F1\">NSRect<\/span><span style=\"color: #F6F6F4\">(<\/span><span style=\"color: #97E1F1\">x<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #BF9EEE\">0<\/span><span style=\"color: #F6F6F4\">, <\/span><span style=\"color: #97E1F1\">y<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #BF9EEE\">0<\/span><span style=\"color: #F6F6F4\">, <\/span><span style=\"color: #97E1F1\">width<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #BF9EEE\">400<\/span><span style=\"color: #F6F6F4\">, <\/span><span style=\"color: #97E1F1\">height<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #BF9EEE\">500<\/span><span style=\"color: #F6F6F4\">),<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">                <\/span><span style=\"color: #97E1F1\">isPresented<\/span><span style=\"color: #F6F6F4\">: $isPresented,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">                <\/span><span style=\"color: #97E1F1\">content<\/span><span style=\"color: #F6F6F4\">: content<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            )<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<p>Cette approche permet de garder un code SwiftUI propre et lisible, tout en d\u00e9l\u00e9guant la complexit\u00e9 \u00e0 AppKit.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Exposer le panel avec une extension SwiftUI<\/h2>\n\n\n\n<p>Une fois le <code>FloatingPanelModifier<\/code> en place, il reste encore une \u00e9tape importante : proposer une API simple \u00e0 utiliser depuis les vues SwiftUI.<\/p>\n\n\n\n<p>L\u2019id\u00e9e est d\u2019\u00e9viter d\u2019appliquer manuellement le modifier \u00e0 chaque fois avec une syntaxe trop verbeuse. \u00c0 la place, on peut ajouter une extension sur <code>View<\/code> pour exposer une m\u00e9thode d\u00e9di\u00e9e, plus lisible et plus naturelle \u00e0 utiliser dans le code.<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#f6f6f4;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>extension View {\n\n    func floatingPanel&lt;Content: View>(\n        isPresented: Binding&lt;Bool>,\n        @ViewBuilder content: @escaping () -> Content\n    ) -> some View {\n        self.modifier(\n            FloatingPanelModifier(\n                isPresented: isPresented,\n                content: content\n            )\n        )\n    }\n    \n}<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki dracula-soft\" style=\"background-color: #282A36\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #F286C4\">extension<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1; font-style: italic\">View<\/span><span style=\"color: #F6F6F4\"> {<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">func<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #62E884\">floatingPanel<\/span><span style=\"color: #F6F6F4\">&lt;<\/span><span style=\"color: #BF9EEE; font-style: italic\">Content<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #97E1F1; font-style: italic\">View<\/span><span style=\"color: #F6F6F4\">&gt;(<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #62E884; font-style: italic\">isPresented<\/span><span style=\"color: #F6F6F4\">: Binding&lt;<\/span><span style=\"color: #97E1F1; font-style: italic\">Bool<\/span><span style=\"color: #F6F6F4\">&gt;,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        @<\/span><span style=\"color: #62E884\">ViewBuilder<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #FFB86C; font-style: italic\">content<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #F286C4\">@escaping<\/span><span style=\"color: #F6F6F4\"> () <\/span><span style=\"color: #F286C4\">-&gt;<\/span><span style=\"color: #F6F6F4\"> Content<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    ) <\/span><span style=\"color: #F286C4\">-&gt;<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">some<\/span><span style=\"color: #F6F6F4\"> View {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #BF9EEE; font-style: italic\">self<\/span><span style=\"color: #F6F6F4\">.<\/span><span style=\"color: #97E1F1\">modifier<\/span><span style=\"color: #F6F6F4\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            <\/span><span style=\"color: #97E1F1\">FloatingPanelModifier<\/span><span style=\"color: #F6F6F4\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">                <\/span><span style=\"color: #97E1F1\">isPresented<\/span><span style=\"color: #F6F6F4\">: isPresented,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">                <\/span><span style=\"color: #97E1F1\">content<\/span><span style=\"color: #F6F6F4\">: content<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            )<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        )<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<p>Cette extension ne fait pas grand-chose en apparence, mais elle joue un r\u00f4le important dans la qualit\u00e9 de l\u2019int\u00e9gration. Elle permet de masquer la complexit\u00e9 du <code>ViewModifier<\/code> derri\u00e8re une extension beaucoup plus expressive.<\/p>\n\n\n\n<p>Une fois en place, l\u2019utilisation devient tr\u00e8s simple :<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#f6f6f4;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>@State private var showPanel = false\n\nButton(\"Voir le d\u00e9tail\") {\n    showPanel.toggle()\n}\n.floatingPanel(isPresented: $showPanel) {\n    DetailView()\n}<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki dracula-soft\" style=\"background-color: #282A36\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #F286C4\">@State<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">private<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">var<\/span><span style=\"color: #F6F6F4\"> showPanel <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">false<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #97E1F1\">Button<\/span><span style=\"color: #F6F6F4\">(<\/span><span style=\"color: #DEE492\">&quot;<\/span><span style=\"color: #E7EE98\">Voir le d\u00e9tail<\/span><span style=\"color: #DEE492\">&quot;<\/span><span style=\"color: #F6F6F4\">) {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    showPanel.<\/span><span style=\"color: #97E1F1\">toggle<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">}<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">.<\/span><span style=\"color: #97E1F1\">floatingPanel<\/span><span style=\"color: #F6F6F4\">(<\/span><span style=\"color: #97E1F1\">isPresented<\/span><span style=\"color: #F6F6F4\">: $showPanel) {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #97E1F1\">DetailView<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<p>On retrouve ici toute la simplicit\u00e9 de SwiftUI, avec en arri\u00e8re-plan une impl\u00e9mentation robuste bas\u00e9e sur <code>NSPanel<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">G\u00e9rer le cycle de vie : fermer le panel correctement<\/h2>\n\n\n\n<p>L\u2019un des principaux d\u00e9fis rencontr\u00e9s concerne la fermeture du panel.<\/p>\n\n\n\n<p>Par d\u00e9faut, un <code>NSPanel<\/code> peut rester affich\u00e9 m\u00eame si la vue principale dispara\u00eet. Dans une application en menu bar, ce comportement devient rapidement probl\u00e9matique.<\/p>\n\n\n\n<p>Pour r\u00e9pondre \u00e0 ce besoin, j\u2019ai choisi de fermer automatiquement le panel d\u00e8s qu\u2019il perd le focus.<\/p>\n\n\n\n<p>Cela se fait directement dans la sous-classe <code>FloatingPanel<\/code>, en surchargeant la m\u00e9thode <code>resignKey<\/code> :<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#f6f6f4;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>override func resignKey() {\n    super.resignKey()\n\n    if isVisible {\n        close()\n    }\n}<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki dracula-soft\" style=\"background-color: #282A36\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #F286C4\">override<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">func<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #62E884\">resignKey<\/span><span style=\"color: #F6F6F4\">() {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #BF9EEE; font-style: italic\">super<\/span><span style=\"color: #F6F6F4\">.<\/span><span style=\"color: #97E1F1\">resignKey<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">if<\/span><span style=\"color: #F6F6F4\"> isVisible {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #97E1F1\">close<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    }<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<p>Cette m\u00e9thode est appel\u00e9e d\u00e8s que le panel n\u2019est plus la fen\u00eatre active, par exemple lorsqu\u2019un utilisateur clique en dehors.<\/p>\n\n\n\n<p>Le comportement devient alors beaucoup plus naturel, le panel agit comme une interface contextuelle, qui appara\u00eet lorsqu\u2019on en a besoin, puis dispara\u00eet automatiquement d\u00e8s que l\u2019on passe \u00e0 autre chose.<\/p>\n\n\n\n<p>Ce type de d\u00e9tail peut sembler mineur, mais il joue un r\u00f4le essentiel dans la perception globale de l\u2019application. Sans cette logique, on se retrouve facilement avec des panels persistants qui donnent une impression d\u2019interface mal ma\u00eetris\u00e9e.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Positionner le panel \u00e0 c\u00f4t\u00e9 de la fen\u00eatre principale<\/h2>\n\n\n\n<p>Une autre difficult\u00e9 importante dans cette impl\u00e9mentation concerne le positionnement du panel.<\/p>\n\n\n\n<p>Par d\u00e9faut, un <code>NSPanel<\/code> s\u2019ouvre sans tenir compte du contexte visuel de l\u2019application. Dans mon cas, ce n\u2019\u00e9tait pas acceptable. L\u2019objectif \u00e9tait clair : le panel devait appara\u00eetre comme une <strong>extension directe de la fen\u00eatre principale<\/strong>, et non comme une fen\u00eatre ind\u00e9pendante.<\/p>\n\n\n\n<p>Pour cela, j\u2019ai ajout\u00e9 une logique de positionnement directement dans le <code>FloatingPanelModifier<\/code>, afin de calculer dynamiquement la position du panel au moment de son affichage.<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#f6f6f4;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>func positionPanel() {\n    \/\/ Si la fen\u00eatre du panel n'existe pas, on ne peut rien faire.\n    guard let panel else { return }\n\n    \/\/ Si la fen\u00eatre h\u00f4te n'existe pas, on centre simplement le panel.\n    guard let hostWindow  = NSApplication.shared.windows.first else {\n        panel.center()\n        return\n    }\n\n    let gap: CGFloat = 12\n    let hostFrame = hostWindow.frame\n    let panelSize = panel.frame.size\n\n    \/\/ Zone visible de l\u2019\u00e9cran.\n    \/\/ On prend d'abord l\u2019\u00e9cran de la fen\u00eatre h\u00f4te, puis celui du panel,\n    \/\/ puis l\u2019\u00e9cran principal comme secours.\n    let visibleFrame =\n        hostWindow.screen?.visibleFrame ??\n        panel.screen?.visibleFrame ??\n        NSScreen.main?.visibleFrame ??\n        .zero\n\n    var newOrigin = panel.frame.origin\n\n    \/\/ Positions possibles en X :\n    \/\/ - \u00e0 gauche de la fen\u00eatre h\u00f4te\n    \/\/ - \u00e0 droite de la fen\u00eatre h\u00f4te\n    let leftX = hostFrame.minX - gap - panelSize.width\n    let rightX = hostFrame.maxX + gap\n\n    if leftX >= visibleFrame.minX {\n        \/\/ On pr\u00e9f\u00e8re afficher le panel \u00e0 gauche si \u00e7a rentre.\n        newOrigin.x = leftX\n    } else if rightX + panelSize.width &lt;= visibleFrame.maxX {\n        \/\/ Sinon, on essaye \u00e0 droite.\n        newOrigin.x = rightX\n    } else {\n        \/\/ Sinon, on le garde dans la zone visible, m\u00eame s\u2019il ne peut pas\n        \/\/ \u00eatre parfaitement plac\u00e9 \u00e0 gauche ou \u00e0 droite.\n        newOrigin.x = min(\n            max(rightX, visibleFrame.minX),\n            visibleFrame.maxX - panelSize.width\n        )\n    }\n\n    \/\/ Alignement vertical :\n    \/\/ on aligne le haut du panel avec le haut de la fen\u00eatre h\u00f4te.\n    let topAlignedY = hostFrame.maxY - panelSize.height\n\n    \/\/ On force ensuite la position dans les limites visibles de l\u2019\u00e9cran.\n    newOrigin.y = min(\n        max(topAlignedY, visibleFrame.minY),\n        visibleFrame.maxY - panelSize.height\n    )\n\n    let newFrame = NSRect(origin: newOrigin, size: panelSize)\n    panel.setFrame(newFrame, display: false)\n}<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki dracula-soft\" style=\"background-color: #282A36\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #F286C4\">func<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #62E884\">positionPanel<\/span><span style=\"color: #F6F6F4\">() {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/ Si la fen\u00eatre du panel n&#39;existe pas, on ne peut rien faire.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">guard<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">let<\/span><span style=\"color: #F6F6F4\"> panel <\/span><span style=\"color: #F286C4\">else<\/span><span style=\"color: #F6F6F4\"> { <\/span><span style=\"color: #F286C4\">return<\/span><span style=\"color: #F6F6F4\"> }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/ Si la fen\u00eatre h\u00f4te n&#39;existe pas, on centre simplement le panel.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">guard<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">let<\/span><span style=\"color: #F6F6F4\"> hostWindow  <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> NSApplication.shared.windows.<\/span><span style=\"color: #BF9EEE\">first<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">else<\/span><span style=\"color: #F6F6F4\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        panel.<\/span><span style=\"color: #97E1F1\">center<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #F286C4\">return<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">let<\/span><span style=\"color: #F6F6F4\"> gap: CGFloat <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #BF9EEE\">12<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">let<\/span><span style=\"color: #F6F6F4\"> hostFrame <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> hostWindow.frame<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">let<\/span><span style=\"color: #F6F6F4\"> panelSize <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> panel.frame.<\/span><span style=\"color: #BF9EEE\">size<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/ Zone visible de l\u2019\u00e9cran.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/ On prend d&#39;abord l\u2019\u00e9cran de la fen\u00eatre h\u00f4te, puis celui du panel,<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/ puis l\u2019\u00e9cran principal comme secours.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">let<\/span><span style=\"color: #F6F6F4\"> visibleFrame <\/span><span style=\"color: #F286C4\">=<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        hostWindow.screen<\/span><span style=\"color: #F286C4\">?<\/span><span style=\"color: #F6F6F4\">.visibleFrame <\/span><span style=\"color: #F286C4\">??<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        panel.screen<\/span><span style=\"color: #F286C4\">?<\/span><span style=\"color: #F6F6F4\">.visibleFrame <\/span><span style=\"color: #F286C4\">??<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        NSScreen.main<\/span><span style=\"color: #F286C4\">?<\/span><span style=\"color: #F6F6F4\">.visibleFrame <\/span><span style=\"color: #F286C4\">??<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        .zero<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">var<\/span><span style=\"color: #F6F6F4\"> newOrigin <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> panel.frame.origin<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/ Positions possibles en X :<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/ - \u00e0 gauche de la fen\u00eatre h\u00f4te<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/ - \u00e0 droite de la fen\u00eatre h\u00f4te<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">let<\/span><span style=\"color: #F6F6F4\"> leftX <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> hostFrame.minX <\/span><span style=\"color: #F286C4\">-<\/span><span style=\"color: #F6F6F4\"> gap <\/span><span style=\"color: #F286C4\">-<\/span><span style=\"color: #F6F6F4\"> panelSize.width<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">let<\/span><span style=\"color: #F6F6F4\"> rightX <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> hostFrame.maxX <\/span><span style=\"color: #F286C4\">+<\/span><span style=\"color: #F6F6F4\"> gap<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">if<\/span><span style=\"color: #F6F6F4\"> leftX <\/span><span style=\"color: #F286C4\">&gt;=<\/span><span style=\"color: #F6F6F4\"> visibleFrame.minX {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #7B7F8B\">\/\/ On pr\u00e9f\u00e8re afficher le panel \u00e0 gauche si \u00e7a rentre.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        newOrigin.x <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> leftX<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    } <\/span><span style=\"color: #F286C4\">else<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #F286C4\">if<\/span><span style=\"color: #F6F6F4\"> rightX <\/span><span style=\"color: #F286C4\">+<\/span><span style=\"color: #F6F6F4\"> panelSize.width <\/span><span style=\"color: #F286C4\">&lt;=<\/span><span style=\"color: #F6F6F4\"> visibleFrame.maxX {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #7B7F8B\">\/\/ Sinon, on essaye \u00e0 droite.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        newOrigin.x <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> rightX<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    } <\/span><span style=\"color: #F286C4\">else<\/span><span style=\"color: #F6F6F4\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #7B7F8B\">\/\/ Sinon, on le garde dans la zone visible, m\u00eame s\u2019il ne peut pas<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #7B7F8B\">\/\/ \u00eatre parfaitement plac\u00e9 \u00e0 gauche ou \u00e0 droite.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        newOrigin.x <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1\">min<\/span><span style=\"color: #F6F6F4\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            <\/span><span style=\"color: #97E1F1\">max<\/span><span style=\"color: #F6F6F4\">(rightX, visibleFrame.minX),<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">            visibleFrame.maxX <\/span><span style=\"color: #F286C4\">-<\/span><span style=\"color: #F6F6F4\"> panelSize.width<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        )<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/ Alignement vertical :<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/ on aligne le haut du panel avec le haut de la fen\u00eatre h\u00f4te.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">let<\/span><span style=\"color: #F6F6F4\"> topAlignedY <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> hostFrame.maxY <\/span><span style=\"color: #F286C4\">-<\/span><span style=\"color: #F6F6F4\"> panelSize.height<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #7B7F8B\">\/\/ On force ensuite la position dans les limites visibles de l\u2019\u00e9cran.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    newOrigin.y <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1\">min<\/span><span style=\"color: #F6F6F4\">(<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        <\/span><span style=\"color: #97E1F1\">max<\/span><span style=\"color: #F6F6F4\">(topAlignedY, visibleFrame.minY),<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">        visibleFrame.maxY <\/span><span style=\"color: #F286C4\">-<\/span><span style=\"color: #F6F6F4\"> panelSize.height<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    )<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #F286C4\">let<\/span><span style=\"color: #F6F6F4\"> newFrame <\/span><span style=\"color: #F286C4\">=<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #97E1F1\">NSRect<\/span><span style=\"color: #F6F6F4\">(<\/span><span style=\"color: #97E1F1\">origin<\/span><span style=\"color: #F6F6F4\">: newOrigin, <\/span><span style=\"color: #97E1F1\">size<\/span><span style=\"color: #F6F6F4\">: panelSize)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    panel.<\/span><span style=\"color: #97E1F1\">setFrame<\/span><span style=\"color: #F6F6F4\">(newFrame, <\/span><span style=\"color: #97E1F1\">display<\/span><span style=\"color: #F6F6F4\">: <\/span><span style=\"color: #BF9EEE\">false<\/span><span style=\"color: #F6F6F4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<p>L\u2019id\u00e9e ici est de positionner le panel intelligemment en fonction de l\u2019espace disponible \u00e0 l\u2019\u00e9cran.<\/p>\n\n\n\n<p>Plut\u00f4t que de le placer syst\u00e9matiquement \u00e0 droite ou \u00e0 gauche, la logique s\u2019adapte automatiquement. Si l\u2019espace est suffisant, le panel s\u2019affiche \u00e0 gauche de la fen\u00eatre principale. Sinon, il se positionne \u00e0 droite. Et si aucune des deux positions n\u2019est id\u00e9ale, il reste contraint dans la zone visible de l\u2019\u00e9cran.<\/p>\n\n\n\n<p>L\u2019alignement vertical suit la m\u00eame logique. Le panel est align\u00e9 avec le haut de la fen\u00eatre principale, puis ajust\u00e9 si n\u00e9cessaire pour rester enti\u00e8rement visible.<\/p>\n\n\n\n<p>Mais pour que ce positionnement soit r\u00e9ellement efficace, il doit \u00eatre appliqu\u00e9 <strong>au bon moment<\/strong>.<\/p>\n\n\n\n<p>Dans mon cas, cette logique est appel\u00e9e juste avant l\u2019affichage du panel, dans la m\u00e9thode <code>present()<\/code> de la classe <code>FloatingPanelModifier<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span role=\"button\" tabindex=\"0\" style=\"color:#f6f6f4;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>func present() {\n    positionPanel()\n    panel?.orderFront(nil)\n    panel?.makeKey()\n}<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki dracula-soft\" style=\"background-color: #282A36\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #F286C4\">func<\/span><span style=\"color: #F6F6F4\"> <\/span><span style=\"color: #62E884\">present<\/span><span style=\"color: #F6F6F4\">() {<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    <\/span><span style=\"color: #97E1F1\">positionPanel<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    panel<\/span><span style=\"color: #F286C4\">?<\/span><span style=\"color: #F6F6F4\">.<\/span><span style=\"color: #97E1F1\">orderFront<\/span><span style=\"color: #F6F6F4\">(<\/span><span style=\"color: #BF9EEE\">nil<\/span><span style=\"color: #F6F6F4\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">    panel<\/span><span style=\"color: #F286C4\">?<\/span><span style=\"color: #F6F6F4\">.<\/span><span style=\"color: #97E1F1\">makeKey<\/span><span style=\"color: #F6F6F4\">()<\/span><\/span>\n<span class=\"line\"><span style=\"color: #F6F6F4\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<div style=\"height:20px\" aria-hidden=\"true\" class=\"wp-block-spacer\"><\/div>\n\n\n\n<p>L\u2019ordre des op\u00e9rations est important. Le panel est d\u2019abord positionn\u00e9, puis affich\u00e9 et activ\u00e9. Cela \u00e9vite un effet visuel o\u00f9 il appara\u00eetrait \u00e0 un endroit avant de se d\u00e9placer, ce qui donnerait une impression d\u2019interface approximative.<\/p>\n\n\n\n<p>Avec cette approche, le panel est imm\u00e9diatement affich\u00e9 au bon endroit, ce qui renforce la sensation de fluidit\u00e9.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Ce que ce choix apporte concr\u00e8tement<\/h2>\n\n\n\n<p>Avec le recul, l\u2019utilisation d\u2019un <code>NSPanel<\/code> dans ce contexte apporte plusieurs b\u00e9n\u00e9fices.<\/p>\n\n\n\n<p>L\u2019interface reste l\u00e9g\u00e8re, tout en permettant d\u2019afficher des informations complexes. L\u2019utilisateur conserve son contexte, ce qui am\u00e9liore la lisibilit\u00e9 et la compr\u00e9hension.<\/p>\n\n\n\n<p>D\u2019un point de vue technique, cette approche permet \u00e9galement de structurer proprement le code, en s\u00e9parant clairement les responsabilit\u00e9s entre SwiftUI et AppKit.<\/p>\n\n\n\n<p>Enfin, elle offre une grande flexibilit\u00e9 pour faire \u00e9voluer l\u2019interface sans remettre en cause l\u2019architecture globale.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Mettre en place un floating panel avec SwiftUI demande un peu de travail, notamment pour g\u00e9rer correctement le cycle de vie et le positionnement.<\/p>\n\n\n\n<p>Mais c\u2019est pr\u00e9cis\u00e9ment ce type de d\u00e9tail qui permet de passer d\u2019une application fonctionnelle \u00e0 une application r\u00e9ellement agr\u00e9able \u00e0 utiliser.<\/p>\n\n\n\n<p>Si vous d\u00e9veloppez une application macOS, en particulier une application bas\u00e9e sur la menu bar, le <strong>NSPanel est une solution particuli\u00e8rement pertinente<\/strong> pour enrichir votre interface sans la complexifier.<\/p>\n\n\n\n<p>Et comme souvent, ce sont ces choix d\u2019architecture et d\u2019exp\u00e9rience utilisateur qui font toute la diff\u00e9rence.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>L\u2019impl\u00e9mentation compl\u00e8te est disponible sur GitHub pour aller plus loin et adapter cette approche \u00e0 vos propres projets : \ud83d\udc47<\/p>\n\n\n<div class=\"vlp-link-container vlp-layout-basic wp-block-visual-link-preview-link\"><a href=\"https:\/\/github.com\/k-angama\/macOS-dev-cache-cleaner\/tree\/main\/DevCacheCleaner\/Common\/Modifiers\" class=\"vlp-link\" title=\"macOS-dev-cache-cleaner\/DevCacheCleaner\/Common\/Modifiers at main \u00b7 k-angama\/macOS-dev-cache-cleaner\" rel=\"nofollow noopener\" target=\"_blank\"><\/a><div class=\"vlp-layout-zone-side\"><div class=\"vlp-block-2 vlp-link-image\"><img decoding=\"async\" src=\"https:\/\/opengraph.githubassets.com\/6025ad7b6f5780f209a56dad23bf85e46acd93957d4a4c8b73b7560a888776d6\/k-angama\/macOS-dev-cache-cleaner\" style=\"max-width: 150px; max-height: 150px\" \/><\/div><\/div><div class=\"vlp-layout-zone-main\"><div class=\"vlp-block-0 vlp-link-title\">macOS-dev-cache-cleaner\/DevCacheCleaner\/Common\/Modifiers at main \u00b7 k-angama\/macOS-dev-cache-cleaner<\/div><div class=\"vlp-block-1 vlp-link-summary\">A macOS menu bar app for inspecting and cleaning developer caches stored in your Home folder. &#8211; k-angama\/macOS-dev-cache-cleaner<\/div><\/div><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n<style>.wp-block-kadence-advancedheading.kt-adv-heading3247_2fe6d2-1e, .wp-block-kadence-advancedheading.kt-adv-heading3247_2fe6d2-1e[data-kb-block=\"kb-adv-heading3247_2fe6d2-1e\"]{font-size:var(--global-kb-font-size-md, 1.25rem);font-style:normal;}.wp-block-kadence-advancedheading.kt-adv-heading3247_2fe6d2-1e mark.kt-highlight, .wp-block-kadence-advancedheading.kt-adv-heading3247_2fe6d2-1e[data-kb-block=\"kb-adv-heading3247_2fe6d2-1e\"] mark.kt-highlight{font-style:normal;color:#f76a0c;-webkit-box-decoration-break:clone;box-decoration-break:clone;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;}.wp-block-kadence-advancedheading.kt-adv-heading3247_2fe6d2-1e img.kb-inline-image, .wp-block-kadence-advancedheading.kt-adv-heading3247_2fe6d2-1e[data-kb-block=\"kb-adv-heading3247_2fe6d2-1e\"] img.kb-inline-image{width:150px;vertical-align:baseline;}<\/style>\n<h4 class=\"kt-adv-heading3247_2fe6d2-1e wp-block-kadence-advancedheading\" data-kb-block=\"kb-adv-heading3247_2fe6d2-1e\">Article similaire<\/h4>\n\n\n\n<ul class=\"wp-block-list\">\n<li style=\"margin-top:var(--wp--preset--spacing--20);margin-right:var(--wp--preset--spacing--60);margin-bottom:var(--wp--preset--spacing--20);margin-left:var(--wp--preset--spacing--60)\"><a href=\"https:\/\/www.kangama.com\/outil-macos-nettoyer-cache-developpeur\/\" data-type=\"post\" data-id=\"3105\">Cr\u00e9er un outil macOS pour nettoyer les caches d\u00e9veloppeur : retour d\u2019exp\u00e9rience<\/a><\/li>\n\n\n\n<li style=\"margin-right:var(--wp--preset--spacing--60);margin-left:var(--wp--preset--spacing--60)\"><a href=\"https:\/\/www.kangama.com\/menu-bar-macos-swiftui-guide-pratique\/\" data-type=\"link\" data-id=\"https:\/\/www.kangama.com\/menu-bar-macos-swiftui-guide-pratique\/\">Comment cr\u00e9er une menu bar utile sur macOS avec SwiftUI<\/a><\/li>\n\n\n\n<li style=\"margin-right:var(--wp--preset--spacing--60);margin-left:var(--wp--preset--spacing--60)\"><a href=\"https:\/\/www.kangama.com\/devcachecleaner-workspace-macos\/\" data-type=\"link\" data-id=\"https:\/\/www.kangama.com\/menu-bar-macos-swiftui-guide-pratique\/\">DevCacheCleaner 0.2.2-alpha : nettoyer aussi les dossiers g\u00e9n\u00e9r\u00e9s de vos projets<\/a><\/li>\n\n\n\n<li style=\"margin-right:var(--wp--preset--spacing--60);margin-left:var(--wp--preset--spacing--60)\"><a href=\"https:\/\/www.kangama.com\/smappservice-swiftui-app-macos-demarrage\/\" data-type=\"link\" data-id=\"https:\/\/www.kangama.com\/menu-bar-macos-swiftui-guide-pratique\/\">SMAppService SwiftUI : lancer une application macOS au d\u00e9marrage<\/a><\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n<style>.kb-row-layout-id3247_142303-12 > .kt-row-column-wrap{align-content:center;}:where(.kb-row-layout-id3247_142303-12 > .kt-row-column-wrap) > .wp-block-kadence-column{justify-content:center;}.kb-row-layout-id3247_142303-12 > .kt-row-column-wrap{column-gap:var(--global-kb-gap-lg, 4rem);row-gap:var(--global-kb-gap-lg, 4rem);max-width:770px;margin-left:auto;margin-right:auto;padding-top:var(--global-kb-spacing-xxl, 5rem);padding-bottom:var(--global-kb-spacing-xxl, 5rem);grid-template-columns:minmax(0, 1fr);}.kb-row-layout-id3247_142303-12{background-color:var(--global-palette8, #F7FAFC);background-image:url('https:\/\/www.kangama.com\/wp-content\/uploads\/2021\/12\/me-contacter-scaled.jpg');background-size:cover;background-position:center center;background-attachment:fixed;background-repeat:no-repeat;}.kb-row-layout-id3247_142303-12 > .kt-row-layout-overlay{opacity:0.50;background-color:var(--ast-global-color-8);}.kb-row-layout-id3247_142303-12 ,.kb-row-layout-id3247_142303-12 h1,.kb-row-layout-id3247_142303-12 h2,.kb-row-layout-id3247_142303-12 h3,.kb-row-layout-id3247_142303-12 h4,.kb-row-layout-id3247_142303-12 h5,.kb-row-layout-id3247_142303-12 h6{color:var(--global-palette3, #1A202C);}.kb-row-layout-id3247_142303-12 a{color:var(--global-palette1, #3182CE);}.kb-row-layout-id3247_142303-12 a:hover{color:var(--global-palette2, #2B6CB0);}@media all and (max-width: 1024px), only screen and (min-device-width: 1024px) and (max-device-width: 1366px) and (-webkit-min-device-pixel-ratio: 2) and (hover: none){.kb-row-layout-id3247_142303-12{background-attachment:scroll;}}@media all and (max-width: 1024px){.kb-row-layout-id3247_142303-12 > .kt-row-column-wrap{grid-template-columns:minmax(0, 1fr);}}@media all and (max-width: 767px){.kb-row-layout-id3247_142303-12 > .kt-row-column-wrap{grid-template-columns:minmax(0, 1fr);}}<\/style><div class=\"kb-row-layout-wrap kb-row-layout-id3247_142303-12 alignnone has-theme-palette8-background-color kt-row-has-bg wp-block-kadence-rowlayout\"><div class=\"kt-row-layout-overlay kt-row-overlay-normal\"><\/div><div class=\"kt-row-column-wrap kt-has-1-columns kt-row-layout-equal kt-tab-layout-inherit kt-mobile-layout-row kt-row-valign-middle\">\n<style>.kadence-column3247_a5fb22-7c > .kt-inside-inner-col{display:flex;}.kadence-column3247_a5fb22-7c > .kt-inside-inner-col,.kadence-column3247_a5fb22-7c > .kt-inside-inner-col:before{border-top-left-radius:0px;border-top-right-radius:0px;border-bottom-right-radius:0px;border-bottom-left-radius:0px;}.kadence-column3247_a5fb22-7c > .kt-inside-inner-col{column-gap:var(--global-kb-gap-sm, 1rem);}.kadence-column3247_a5fb22-7c > .kt-inside-inner-col{flex-direction:column;justify-content:center;}.kadence-column3247_a5fb22-7c > .kt-inside-inner-col > .aligncenter{width:100%;}.kt-row-column-wrap > .kadence-column3247_a5fb22-7c{align-self:center;}.kt-inner-column-height-full:not(.kt-has-1-columns) > .wp-block-kadence-column.kadence-column3247_a5fb22-7c{align-self:auto;}.kt-inner-column-height-full:not(.kt-has-1-columns) > .wp-block-kadence-column.kadence-column3247_a5fb22-7c > .kt-inside-inner-col{flex-direction:column;justify-content:center;}.kadence-column3247_a5fb22-7c > .kt-inside-inner-col:before{opacity:0.3;}.kadence-column3247_a5fb22-7c{text-align:center;}.kadence-column3247_a5fb22-7c{position:relative;}@media all and (max-width: 1024px){.kt-row-column-wrap > .kadence-column3247_a5fb22-7c{align-self:center;}}@media all and (max-width: 1024px){.kt-inner-column-height-full:not(.kt-has-1-columns) > .wp-block-kadence-column.kadence-column3247_a5fb22-7c{align-self:auto;}}@media all and (max-width: 1024px){.kt-inner-column-height-full:not(.kt-has-1-columns) > .wp-block-kadence-column.kadence-column3247_a5fb22-7c > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}@media all and (max-width: 1024px){.kadence-column3247_a5fb22-7c > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}@media all and (max-width: 767px){.kt-row-column-wrap > .kadence-column3247_a5fb22-7c{align-self:center;}.kt-inner-column-height-full:not(.kt-has-1-columns) > .wp-block-kadence-column.kadence-column3247_a5fb22-7c{align-self:auto;}.kt-inner-column-height-full:not(.kt-has-1-columns) > .wp-block-kadence-column.kadence-column3247_a5fb22-7c > .kt-inside-inner-col{flex-direction:column;justify-content:center;}.kadence-column3247_a5fb22-7c > .kt-inside-inner-col{flex-direction:column;justify-content:center;}}<\/style>\n<div class=\"wp-block-kadence-column kadence-column3247_a5fb22-7c\"><div class=\"kt-inside-inner-col\"><style>.wp-block-kadence-advancedheading.kt-adv-heading3247_90000b-5a, .wp-block-kadence-advancedheading.kt-adv-heading3247_90000b-5a[data-kb-block=\"kb-adv-heading3247_90000b-5a\"]{margin-top:0px;margin-bottom:var(--global-kb-spacing-lg, 3rem);text-align:center;font-size:var(--global-kb-font-size-lg, 2rem);line-height:1.2em;font-style:normal;}.wp-block-kadence-advancedheading.kt-adv-heading3247_90000b-5a mark.kt-highlight, .wp-block-kadence-advancedheading.kt-adv-heading3247_90000b-5a[data-kb-block=\"kb-adv-heading3247_90000b-5a\"] mark.kt-highlight{font-style:normal;color:#f76a0c;-webkit-box-decoration-break:clone;box-decoration-break:clone;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;}.wp-block-kadence-advancedheading.kt-adv-heading3247_90000b-5a img.kb-inline-image, .wp-block-kadence-advancedheading.kt-adv-heading3247_90000b-5a[data-kb-block=\"kb-adv-heading3247_90000b-5a\"] img.kb-inline-image{width:150px;vertical-align:baseline;}<\/style>\n<h2 class=\"kt-adv-heading3247_90000b-5a wp-block-kadence-advancedheading has-ast-global-color-5-color has-text-color\" data-kb-block=\"kb-adv-heading3247_90000b-5a\"><strong><strong>Besoin d\u2019un regard technique sur votre projet ?<br>Je peux vous accompagner sur vos d\u00e9veloppements.<\/strong><\/strong><\/h2>\n\n\n<style>.wp-block-kadence-advancedbtn.kb-btns3247_bebe25-b4{gap:var(--global-kb-gap-xs, 0.5rem );justify-content:center;align-items:center;}.kt-btns3247_bebe25-b4 .kt-button{font-weight:normal;font-style:normal;}.kt-btns3247_bebe25-b4 .kt-btn-wrap-0{margin-right:5px;}.wp-block-kadence-advancedbtn.kt-btns3247_bebe25-b4 .kt-btn-wrap-0 .kt-button{color:#555555;border-color:#555555;}.wp-block-kadence-advancedbtn.kt-btns3247_bebe25-b4 .kt-btn-wrap-0 .kt-button:hover, .wp-block-kadence-advancedbtn.kt-btns3247_bebe25-b4 .kt-btn-wrap-0 .kt-button:focus{color:#ffffff;border-color:#444444;}.wp-block-kadence-advancedbtn.kt-btns3247_bebe25-b4 .kt-btn-wrap-0 .kt-button::before{display:none;}.wp-block-kadence-advancedbtn.kt-btns3247_bebe25-b4 .kt-btn-wrap-0 .kt-button:hover, .wp-block-kadence-advancedbtn.kt-btns3247_bebe25-b4 .kt-btn-wrap-0 .kt-button:focus{background:#444444;}<\/style>\n<div class=\"wp-block-kadence-advancedbtn kb-buttons-wrap kb-btns3247_bebe25-b4\"><style>ul.menu .wp-block-kadence-advancedbtn .kb-btn3247_3467a6-6a.kb-button{width:initial;}.wp-block-kadence-advancedbtn .kb-btn3247_3467a6-6a.kb-button{color:var(--ast-global-color-7);background:rgba(0,0,0,0);font-weight:bold;text-transform:uppercase;border-top:2px solid var(--ast-global-color-7);border-right:2px solid var(--ast-global-color-7);border-bottom:2px solid var(--ast-global-color-7);border-left:2px solid var(--ast-global-color-7);}.wp-block-kadence-advancedbtn .kb-btn3247_3467a6-6a.kb-button:hover, .wp-block-kadence-advancedbtn .kb-btn3247_3467a6-6a.kb-button:focus{color:var(--ast-global-color-5);background:var(--ast-global-color-7);}@media all and (max-width: 1024px){.wp-block-kadence-advancedbtn .kb-btn3247_3467a6-6a.kb-button{border-top:2px solid var(--ast-global-color-7);border-right:2px solid var(--ast-global-color-7);border-bottom:2px solid var(--ast-global-color-7);border-left:2px solid var(--ast-global-color-7);}}@media all and (max-width: 767px){.wp-block-kadence-advancedbtn .kb-btn3247_3467a6-6a.kb-button{border-top:2px solid var(--ast-global-color-7);border-right:2px solid var(--ast-global-color-7);border-bottom:2px solid var(--ast-global-color-7);border-left:2px solid var(--ast-global-color-7);}}<\/style><a class=\"kb-button kt-button button kb-btn3247_3467a6-6a kt-btn-size-standard kt-btn-width-type-auto kb-btn-global-inherit  kt-btn-has-text-true kt-btn-has-svg-false  wp-block-button__link wp-block-kadence-singlebtn\" href=\"https:\/\/www.kangama.com\/contact\/\"><span class=\"kt-btn-inner-text\">\u00c9changeons sur votre projet<\/span><\/a><\/div>\n<\/div><\/div>\n\n<\/div><\/div>","protected":false},"excerpt":{"rendered":"<p>Cr\u00e9er un floating panel macOS avec SwiftUI et NSPanel pour afficher une vue de d\u00e9tail fluide et int\u00e9gr\u00e9e.<\/p>","protected":false},"author":1,"featured_media":3268,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"set","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[16],"tags":[22,21],"class_list":["post-3247","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-developpement-logiciel-technique","tag-macos","tag-swiftui"],"_links":{"self":[{"href":"https:\/\/www.kangama.com\/en\/wp-json\/wp\/v2\/posts\/3247","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.kangama.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.kangama.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.kangama.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.kangama.com\/en\/wp-json\/wp\/v2\/comments?post=3247"}],"version-history":[{"count":37,"href":"https:\/\/www.kangama.com\/en\/wp-json\/wp\/v2\/posts\/3247\/revisions"}],"predecessor-version":[{"id":3548,"href":"https:\/\/www.kangama.com\/en\/wp-json\/wp\/v2\/posts\/3247\/revisions\/3548"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.kangama.com\/en\/wp-json\/wp\/v2\/media\/3268"}],"wp:attachment":[{"href":"https:\/\/www.kangama.com\/en\/wp-json\/wp\/v2\/media?parent=3247"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.kangama.com\/en\/wp-json\/wp\/v2\/categories?post=3247"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.kangama.com\/en\/wp-json\/wp\/v2\/tags?post=3247"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}