Skip to main content

swiftui_native/
hosting.rs

1//! The bridge that puts a SwiftUI view inside a native view.
2//!
3//! AppKit has `NSHostingView`, a view you can use directly. UIKit has no
4//! equivalent: the only bridge is `UIHostingController`, and the view comes out
5//! of its `.view` property. So on iOS this type holds both — the controller,
6//! because that is what owns `rootView` and answers `sizeThatFits`, and the
7//! view, because that is what everything else wants.
8
9use crate::abi;
10use crate::objc::{self, NSRect};
11use crate::resolve;
12use crate::views::ViewHandle;
13use core::ffi::c_void;
14
15/// A hosting view whose root view can be replaced at any time.
16pub struct HostingView {
17    /// The `NSView`/`UIView` SwiftUI draws into.
18    ptr: *mut c_void,
19    /// On UIKit the SwiftUI side lives on the controller, not the view.
20    #[cfg(not(target_os = "macos"))]
21    controller: *mut c_void,
22    /// The offscreen window this view is captured in, made on first use.
23    ///
24    /// One per hosting view, not one per capture. Per capture leaks a window
25    /// every frame — an application holds on to every window it makes — and
26    /// closing them instead detaches the view between frames, which stops an
27    /// animation dead. One window that lives exactly as long as the view it
28    /// hosts does neither.
29    window: core::cell::Cell<*mut c_void>,
30}
31
32impl HostingView {
33    /// Wrap a view in a fresh hosting view.
34    #[cfg(target_os = "macos")]
35    pub fn new(view: &ViewHandle) -> Self {
36        let _pool = objc::AutoreleasePool::new();
37        Self {
38            ptr: unsafe { make_hosting_view(view) },
39            window: core::cell::Cell::new(core::ptr::null_mut()),
40        }
41    }
42
43    /// Wrap a view in a fresh hosting controller, and take its view.
44    #[cfg(not(target_os = "macos"))]
45    pub fn new(view: &ViewHandle) -> Self {
46        let _pool = objc::AutoreleasePool::new();
47        unsafe {
48            let controller = make_hosting_controller(view);
49            // Asking for `.view` is what makes the controller load it.
50            let ptr = objc::send(controller, objc::sel(c"view"));
51            assert!(!ptr.is_null(), "UIHostingController produced no view");
52            Self { ptr, controller, window: core::cell::Cell::new(core::ptr::null_mut()) }
53        }
54    }
55
56    /// The native view SwiftUI draws into.
57    pub fn as_ptr(&self) -> *mut c_void {
58        self.ptr
59    }
60
61    /// The object that owns `rootView`: the view on AppKit, the controller on UIKit.
62    fn root_owner(&self) -> *mut c_void {
63        #[cfg(target_os = "macos")]
64        {
65            self.ptr
66        }
67        #[cfg(not(target_os = "macos"))]
68        {
69            self.controller
70        }
71    }
72
73    /// The view controller, on platforms that have one.
74    #[cfg(not(target_os = "macos"))]
75    pub fn controller(&self) -> *mut c_void {
76        self.controller
77    }
78
79    /// Swap the root view. SwiftUI diffs the new tree against the old one and
80    /// updates the rendered output in place — this is how a Rust-driven UI
81    /// changes after it is on screen.
82    pub fn set_root(&self, view: &ViewHandle) {
83        let _pool = objc::AutoreleasePool::new();
84        unsafe {
85            // `rootView` is a stored property on a generic class, so the setter
86            // takes the new value indirectly (x0) with `self` in the Swift self
87            // register (x20). It is reached through its dispatch thunk.
88            // The setter consumes the value, and `to_anyview_word` already
89            // mints a `+1` — it boxes a copy of the handle's own value rather
90            // than lending the handle's reference, as it did when a handle *was*
91            // an `AnyView`. The retain that used to balance this is now one too
92            // many.
93            let mut incoming: u64 = view.to_anyview_word();
94            core::arch::asm!("blr {f}", f = in(reg) resolve::hosting_root_setter(),
95                in("x0") &mut incoming as *mut u64,
96                in("x20") self.root_owner(),
97                lateout("x1") _, lateout("x2") _, lateout("x3") _,
98                lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
99                lateout("x8") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
100                lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
101                lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
102                clobber_abi("C"),
103            );
104        }
105    }
106
107    /// Lay the view out at a fixed size.
108    pub fn set_frame(&self, width: f64, height: f64) {
109        let _pool = objc::AutoreleasePool::new();
110        unsafe {
111            objc::send_rect(
112                self.ptr,
113                objc::sel(c"setFrame:"),
114                NSRect::new(0.0, 0.0, width, height),
115            );
116            #[cfg(target_os = "macos")]
117            objc::send(self.ptr, objc::sel(c"layoutSubtreeIfNeeded"));
118            #[cfg(not(target_os = "macos"))]
119            {
120                objc::send(self.ptr, objc::sel(c"setNeedsLayout"));
121                objc::send(self.ptr, objc::sel(c"layoutIfNeeded"));
122            }
123        }
124    }
125
126    /// Whether SwiftUI should inset its content for the window's safe area.
127    ///
128    /// A hosting controller reads the safe area of whatever window it ends up
129    /// in, so on a device with a notch a view asked for at 240x160 comes back
130    /// with a band across the top that has nothing to do with the view. For
131    /// rendering that is noise; for an app it is the point. `safeAreaRegions`
132    /// is the supported way to say which you want.
133    #[cfg(not(target_os = "macos"))]
134    pub fn set_safe_area(&self, enabled: bool) {
135        unsafe {
136            // `SafeAreaRegions` is an OptionSet over a single word; the empty
137            // set is zero and `.all` is every bit.
138            let regions: u64 = if enabled { u64::MAX } else { 0 };
139            core::arch::asm!("blr {f}", f = in(reg) resolve::hosting_ctrl_safe_area(),
140                in("x0") regions,
141                in("x20") self.controller,
142                lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
143                lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
144                lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
145                lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
146                lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
147            );
148        }
149    }
150
151    /// The size SwiftUI would like this view to be.
152    #[cfg(target_os = "macos")]
153    pub fn fitting_size(&self) -> (f64, f64) {
154        let s = unsafe { objc::send_ret_size(self.ptr, objc::sel(c"fittingSize")) };
155        (s.width, s.height)
156    }
157
158    /// The size SwiftUI would like this view to be.
159    ///
160    /// `UIHostingController.sizeThatFits(in:)` rather than the view's own: only
161    /// the controller asks SwiftUI. A zero target is UIKit's
162    /// `layoutFittingCompressedSize`, i.e. "as small as you can be".
163    #[cfg(not(target_os = "macos"))]
164    pub fn fitting_size(&self) -> (f64, f64) {
165        unsafe {
166            let (w, h): (f64, f64);
167            core::arch::asm!("blr {f}", f = in(reg) resolve::hosting_ctrl_size(),
168                in("d0") 0.0f64, in("d1") 0.0f64,
169                in("x20") self.controller,
170                lateout("d0") w, lateout("d1") h,
171                lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
172                lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
173                lateout("x8") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
174                lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
175                lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
176                clobber_abi("C"),
177            );
178            (w, h)
179        }
180    }
181}
182
183impl HostingView {
184    /// The offscreen window to capture this view in, created on first use and
185    /// resized to `width` x `height`.
186    ///
187    /// # Safety
188    /// Must be called on the main thread.
189    pub(crate) unsafe fn offscreen(&self, width: f64, height: f64) -> *mut c_void {
190        unsafe { offscreen_window(self, width, height) }
191    }
192}
193
194impl Drop for HostingView {
195    fn drop(&mut self) {
196        let _pool = objc::AutoreleasePool::new();
197        unsafe {
198            let window = self.window.get();
199            if !window.is_null() {
200                #[cfg(target_os = "macos")]
201                {
202                    // Closing takes it out of the application's window list,
203                    // which is the only thing keeping it alive. `release` alone
204                    // frees nothing.
205                    objc::send(window, objc::sel(c"close"));
206                }
207                #[cfg(not(target_os = "macos"))]
208                {
209                    // A `UIWindow` has no `close` — sending it one raises. It
210                    // is hidden and released instead.
211                    objc::send_bool(window, objc::sel(c"setHidden:"), true);
212                    objc::send(window, objc::sel(c"release"));
213                }
214            }
215        }
216        // `init(rootView:)` hands back an owned reference. Anything this was
217        // installed into — a window's content view, a window's root view
218        // controller — took its own, so letting go of ours is safe and is the
219        // difference between a render costing nothing and costing a hosting
220        // view every time.
221        unsafe {
222            #[cfg(target_os = "macos")]
223            objc::send(self.ptr, objc::sel(c"release"));
224            #[cfg(not(target_os = "macos"))]
225            objc::send(self.controller, objc::sel(c"release"));
226        }
227    }
228}
229
230/// Build `NSHostingView<AnyView>` and call `init(rootView:)` under the Swift CC.
231#[cfg(target_os = "macos")]
232unsafe fn make_hosting_view(view: &ViewHandle) -> *mut c_void {
233    // `NSHostingView<V: View>` takes TWO key arguments: `V.Type` and the
234    // `V: View` witness table. The class loads both straight back out of its own
235    // metadata (`ldp x1, x2, [x20, #0x50]` in its init), so passing only
236    // `V.Type` leaves the conformance slot holding register garbage — which
237    // faults deep inside the Swift metadata cache rather than here.
238    unsafe {
239        let meta = abi::generic_metadata2(
240            resolve::hosting_view_ma(),
241            resolve::anyview_meta(),
242            resolve::anyview_wt(),
243        );
244        init_root_view(resolve::hosting_view_init(), meta, view, "NSHostingView")
245    }
246}
247
248/// Build `UIHostingController<AnyView>` and call `init(rootView:)`.
249#[cfg(not(target_os = "macos"))]
250unsafe fn make_hosting_controller(view: &ViewHandle) -> *mut c_void {
251    unsafe {
252        let meta = abi::generic_metadata2(
253            resolve::hosting_ctrl_ma(),
254            resolve::anyview_meta(),
255            resolve::anyview_wt(),
256        );
257        init_root_view(resolve::hosting_ctrl_init(), meta, view, "UIHostingController")
258    }
259}
260
261/// `init(rootView:)` — x0 = @in AnyView, x20 = the class's metadata.
262unsafe fn init_root_view(
263    init: *const c_void,
264    meta: *const c_void,
265    view: &ViewHandle,
266    what: &str,
267) -> *mut c_void {
268    unsafe {
269        let mut storage: u64 = view.to_anyview_word();
270        swift_runtime_sys::RuntimeRaw::swift_retain(storage as *mut c_void);
271        let out: *mut c_void;
272        core::arch::asm!("blr {f}", f = in(reg) init,
273            in("x0") &mut storage as *mut u64,
274            in("x20") meta,
275            lateout("x0") out,
276            lateout("x1") _, lateout("x2") _, lateout("x3") _,
277            lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
278            lateout("x8") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
279            lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
280            lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
281            clobber_abi("C"),
282        );
283        assert!(!out.is_null(), "{what} creation failed");
284        out
285    }
286}
287
288/// Make (once) and size the offscreen window a hosting view is captured in.
289///
290/// Borderless and buffered, so the content rect is the whole window, and pinned
291/// to the light appearance so a capture does not depend on the host's system
292/// setting.
293#[cfg(target_os = "macos")]
294unsafe fn offscreen_window(hv: &HostingView, width: f64, height: f64) -> *mut c_void {
295    unsafe {
296        let bounds = NSRect::new(0.0, 0.0, width, height);
297        let existing = hv.window.get();
298        if !existing.is_null() {
299            type SetFrame = unsafe extern "C" fn(*mut c_void, *const c_void, NSRect, bool);
300            let set_frame: SetFrame = core::mem::transmute(crate::objc::msg_send_addr());
301            set_frame(existing, objc::sel(c"setFrame:display:"), bounds, false);
302            return existing;
303        }
304
305        let _ = objc::shared_app();
306        let win = objc::send(objc::class(c"OBJC_CLASS_$_NSWindow"), objc::sel(c"alloc"));
307        type InitWin =
308            unsafe extern "C" fn(*mut c_void, *const c_void, NSRect, u64, u64, bool) -> *mut c_void;
309        let init: InitWin = core::mem::transmute(crate::objc::msg_send_addr());
310        // borderless | buffered
311        let win = init(
312            win,
313            objc::sel(c"initWithContentRect:styleMask:backing:defer:"),
314            bounds,
315            0,
316            2,
317            false,
318        );
319        let name = objc::nsstring("NSAppearanceNameAqua");
320        let appearance = objc::send_id(
321            objc::class(c"OBJC_CLASS_$_NSAppearance"),
322            objc::sel(c"appearanceNamed:"),
323            name,
324        );
325        if !appearance.is_null() {
326            objc::send_id(win, objc::sel(c"setAppearance:"), appearance);
327        }
328        objc::send_id(win, objc::sel(c"setContentView:"), hv.as_ptr());
329        hv.window.set(win);
330        win
331    }
332}
333
334/// UIKit's equivalent: a window purely so the view gets laid out.
335#[cfg(not(target_os = "macos"))]
336unsafe fn offscreen_window(hv: &HostingView, width: f64, height: f64) -> *mut c_void {
337    unsafe {
338        let bounds = NSRect::new(0.0, 0.0, width, height);
339        let existing = hv.window.get();
340        if !existing.is_null() {
341            objc::send_rect(existing, objc::sel(c"setFrame:"), bounds);
342            return existing;
343        }
344        let window = objc::send(objc::class(c"OBJC_CLASS_$_UIWindow"), objc::sel(c"alloc"));
345        type InitFrame = unsafe extern "C" fn(*mut c_void, *const c_void, NSRect) -> *mut c_void;
346        let init: InitFrame = core::mem::transmute(crate::objc::msg_send_addr());
347        let window = init(window, objc::sel(c"initWithFrame:"), bounds);
348        // The capture is of the view at exactly the size asked for, so the
349        // window's safe area must not push the content around.
350        hv.set_safe_area(false);
351        // As a subview, not as `rootViewController`: a root view controller is
352        // laid out inside the window's safe area, which on a device with a
353        // notch inserts a band at the top that has nothing to do with the view
354        // under test.
355        objc::send_id(window, objc::sel(c"addSubview:"), hv.as_ptr());
356        // Not `makeKeyAndVisible`: the window only has to exist for layout, and
357        // a process spawned without an application should not try to present.
358        objc::send_bool(window, objc::sel(c"setHidden:"), false);
359        hv.window.set(window);
360        window
361    }
362}