diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4fc5fc..653e2e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,11 @@
 - Make `Layer`'s implementation details private; it is now a struct with `as_ptr` and `is_existing` accessor methods.
 - Add support for tvOS, watchOS and visionOS.
 - Use `objc2` internally.
+- Move `Layer` constructors to the type itself.
+  - `appkit::metal_layer_from_ns_view` is now `Layer::from_ns_view`.
+  - `uikit::metal_layer_from_ui_view` is now `Layer::from_ui_view`.
+- Added `Layer::from_layer` to construct a `Layer` from a `CALayer` directly.
+- Fixed layers not automatically resizing to match the super layer they were created from.
 
 # 0.4.0 (2023-10-31)
 - Update `raw-window-handle` dep to `0.6.0`.
diff --git a/Cargo.toml b/Cargo.toml
index a729d21..124b520 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,6 @@
 name = "raw-window-metal"
 version = "0.4.0"
 license = "MIT OR Apache-2.0"
-authors = ["The Gfx-rs Developers"]
 edition = "2021"
 description = "Interop library between Metal and raw-window-handle"
 documentation = "https://docs.rs/raw-window-metal"
@@ -12,14 +11,16 @@ keywords = ["window", "metal", "graphics"]
 categories = ["game-engines", "graphics"]
 exclude = [".github/*"]
 
-[dependencies]
-raw-window-handle = "0.6.0"
-
 [target.'cfg(target_vendor = "apple")'.dependencies]
 objc2 = "0.5.2"
 objc2-foundation = { version = "0.2.2", features = [
-    "NSObjCRuntime",
+    "NSDictionary",
     "NSGeometry",
+    "NSKeyValueObserving",
+    "NSObjCRuntime",
+    "NSString",
+    "NSThread",
+    "NSValue",
 ] }
 objc2-quartz-core = { version = "0.2.2", features = [
     "CALayer",
@@ -27,22 +28,14 @@ objc2-quartz-core = { version = "0.2.2", features = [
     "objc2-metal",
 ] }
 
-[target.'cfg(target_os = "macos")'.dependencies]
-objc2-app-kit = { version = "0.2.2", features = [
-    "NSResponder",
-    "NSView",
-    "NSWindow",
-    "objc2-quartz-core",
-] }
+[dev-dependencies]
+raw-window-handle = "0.6.0"
 
-[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dependencies]
-objc2-ui-kit = { version = "0.2.2", features = [
-    "UIResponder",
-    "UIView",
-    "UIWindow",
-    "UIScreen",
-    "objc2-quartz-core",
-] }
+[target.'cfg(target_os = "macos")'.dev-dependencies]
+objc2-app-kit = { version = "0.2.2", features = ["NSResponder", "NSView"] }
+
+[target.'cfg(all(target_vendor = "apple", not(target_os = "macos")))'.dev-dependencies]
+objc2-ui-kit = { version = "0.2.2", features = ["UIResponder", "UIView"] }
 
 [package.metadata.docs.rs]
 targets = [
diff --git a/README.md b/README.md
index 2c99903..214d26c 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,6 @@ raw-window-metal = "0.4"
 `CAMetalLayer` is the common entrypoint for graphics APIs (e.g `gfx` or `MoltenVK`), but the handles provided by window libraries may not include such a layer.
 This library may extract either this layer or allocate a new one.
 
-Code is mostly extracted from the `gfx-backend-metal` crate.
-
 ## License
 
 Licensed under either of
diff --git a/src/appkit.rs b/src/appkit.rs
deleted file mode 100644
index eeae971..0000000
--- a/src/appkit.rs
+++ /dev/null
@@ -1,70 +0,0 @@
-use core::ffi::c_void;
-use objc2::rc::Retained;
-use objc2::ClassType;
-use objc2_foundation::{NSObject, NSObjectProtocol};
-use objc2_quartz_core::CAMetalLayer;
-use raw_window_handle::AppKitWindowHandle;
-use std::ptr::NonNull;
-
-use crate::Layer;
-
-/// Get or create a new [`Layer`] associated with the given
-/// [`AppKitWindowHandle`].
-///
-/// # Safety
-///
-/// The handle must be valid.
-pub unsafe fn metal_layer_from_handle(handle: AppKitWindowHandle) -> Layer {
-    unsafe { metal_layer_from_ns_view(handle.ns_view) }
-}
-
-/// Get or create a new [`Layer`] associated with the given `NSView`.
-///
-/// # Safety
-///
-/// The view must be a valid instance of `NSView`.
-pub unsafe fn metal_layer_from_ns_view(view: NonNull<c_void>) -> Layer {
-    // SAFETY: Caller ensures that the view is valid.
-    let obj = unsafe { view.cast::<NSObject>().as_ref() };
-
-    // Check if the view is a `CAMetalLayer`.
-    if obj.is_kind_of::<CAMetalLayer>() {
-        // SAFETY: Just checked that the view is a `CAMetalLayer`.
-        let layer = unsafe { view.cast::<CAMetalLayer>().as_ref() };
-        return Layer {
-            layer: layer.retain(),
-            pre_existing: true,
-        };
-    }
-    // Otherwise assume the view is `NSView`.
-    let view = unsafe { view.cast::<objc2_app_kit::NSView>().as_ref() };
-
-    // Check if the view contains a valid `CAMetalLayer`.
-    let existing = unsafe { view.layer() };
-    if let Some(existing) = existing {
-        if existing.is_kind_of::<CAMetalLayer>() {
-            // SAFETY: Just checked that the layer is a `CAMetalLayer`.
-            let layer = unsafe { Retained::cast::<CAMetalLayer>(existing) };
-            return Layer {
-                layer,
-                pre_existing: true,
-            };
-        }
-    }
-
-    // If the layer was not `CAMetalLayer`, allocate a new one for the view.
-    let layer = unsafe { CAMetalLayer::new() };
-    unsafe { view.setLayer(Some(&layer)) };
-    view.setWantsLayer(true);
-    layer.setBounds(view.bounds());
-
-    if let Some(window) = view.window() {
-        let scale_factor = window.backingScaleFactor();
-        layer.setContentsScale(scale_factor);
-    }
-
-    Layer {
-        layer,
-        pre_existing: false,
-    }
-}
diff --git a/src/lib.rs b/src/lib.rs
index 0150a77..8be8366 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,19 +1,117 @@
+//! # Interop between Metal and [`raw-window-handle`]
+//!
+//! Helpers for constructing a [`CAMetalLayer`] from a handle given by [`raw-window-handle`].
+//!
+//! See the [`Layer`] type for details.
+//!
+//!
+//! ## Example
+//!
+//! Create a layer from a window that implements [`HasWindowHandle`].
+//!
+//! ```
+//! use objc2::rc::Retained;
+//! use objc2_quartz_core::CAMetalLayer;
+//! use raw_window_handle::{RawWindowHandle, HasWindowHandle};
+//! use raw_window_metal::Layer;
+//! #
+//! # let mtm = objc2_foundation::MainThreadMarker::new().expect("doc tests to run on main thread");
+//! #
+//! # #[cfg(target_os = "macos")]
+//! # let view = unsafe { objc2_app_kit::NSView::new(mtm) };
+//! # #[cfg(target_os = "macos")]
+//! # let handle = RawWindowHandle::AppKit(raw_window_handle::AppKitWindowHandle::new(std::ptr::NonNull::from(&*view).cast()));
+//! #
+//! # #[cfg(not(target_os = "macos"))]
+//! # let view = unsafe { objc2_ui_kit::UIView::new(mtm) };
+//! #[cfg(not(target_os = "macos"))]
+//! # let handle = RawWindowHandle::UiKit(raw_window_handle::UiKitWindowHandle::new(std::ptr::NonNull::from(&*view).cast()));
+//! # let window = unsafe { raw_window_handle::WindowHandle::borrow_raw(handle) };
+//!
+//! let layer = match window.window_handle().expect("handle available").as_raw() {
+//!     // SAFETY: The handle is a valid `NSView` because it came from `WindowHandle<'_>`.
+//!     RawWindowHandle::AppKit(handle) => unsafe { Layer::from_ns_view(handle.ns_view) },
+//!     // SAFETY: The handle is a valid `UIView` because it came from `WindowHandle<'_>`.
+//!     RawWindowHandle::UiKit(handle) => unsafe { Layer::from_ui_view(handle.ui_view) },
+//!     _ => panic!("unsupported handle"),
+//! };
+//! let layer: *mut CAMetalLayer = layer.as_ptr().cast();
+//! let layer = unsafe { Retained::retain(layer).unwrap() };
+//!
+//! // Use `CAMetalLayer` here.
+//! ```
+//!
+//! [`raw-window-handle`]: https://crates.io/crates/raw-window-handle
+//! [`HasWindowHandle`]: https://docs.rs/raw-window-handle/0.6.2/raw_window_handle/trait.HasWindowHandle.html
+//!
+//!
+//! ## Reasoning behind creating a sublayer
+//!
+//! If a view does not have a `CAMetalLayer` as the root layer (as is the default for most views),
+//! then we're in a bit of a tricky position! We cannot use the existing layer with Metal, so we
+//! must do something else. There are a few options:
+//!
+//! 1. Panic, and require the user to pass a view with a `CAMetalLayer` layer.
+//!
+//!    While this would "work", it doesn't solve the problem, and instead passes the ball onwards to
+//!    the user and ecosystem to figure it out.
+//!
+//! 2. Override the existing layer with a newly created layer.
+//!
+//!    If we overlook that this does not work in UIKit since `UIView`'s `layer` is `readonly`, and
+//!    that as such we will need to do something different there anyhow, this is actually a fairly
+//!    good solution, and was what the original implementation did.
+//!
+//!    It has some problems though, due to:
+//!
+//!    a. Consumers of `raw-window-metal` like Wgpu and Ash in their API design choosing not to
+//!       register a callback with `-[CALayerDelegate displayLayer:]`, but instead leaves it up to
+//!       the user to figure out when to redraw. That is, they rely on other libraries' callbacks
+//!       telling us when to render.
+//!
+//!       (If you were to make an API only for Metal, you would probably make the user provide a
+//!       `render` closure that'd be called in the right situations).
+//!
+//!    b. Overwriting the `layer` on `NSView` makes the view "layer-hosting", see [wantsLayer],
+//!       which disables drawing functionality on the view like `drawRect:`/`updateLayer`.
+//!
+//!    These two in combination makes it basically impossible for crates like Winit to provide a
+//!    robust rendering callback that integrates with the system's built-in mechanisms for
+//!    redrawing, exactly because overwriting the layer would be disabling those mechanisms!
+//!
+//!    [wantsLayer]: https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer?language=objc
+//!
+//! 3. Create a sublayer.
+//!
+//!    `CALayer` has the concept of "sublayers", which we can use instead of overriding the layer.
+//!
+//!    This is also the recommended solution on UIKit, so it's nice that we can use the same
+//!    implementation regardless of operating system.
+//!
+//!    It _might_, however, perform ever so slightly worse than overriding the layer directly.
+//!
+//! 4. Create a new `MTKView` (or a custom view), and add it as a subview.
+//!
+//!    Similar to creating a sublayer (see above), but also provides a bunch of event handling that
+//!    we don't need.
+//!
+//! Option 3 seems like the most robust solution, so this is what this crate does.
+
 #![cfg(target_vendor = "apple")]
-#![allow(clippy::missing_safety_doc)]
 #![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg_hide), doc(cfg_hide(doc)))]
 #![deny(unsafe_op_in_unsafe_fn)]
 
+mod observer;
+
+use crate::observer::ObserverLayer;
 use core::ffi::c_void;
 use core::hash;
 use core::panic::{RefUnwindSafe, UnwindSafe};
-use objc2::rc::Retained;
-use objc2_quartz_core::CAMetalLayer;
-
-#[cfg(any(target_os = "macos", doc))]
-pub mod appkit;
-
-#[cfg(any(not(target_os = "macos"), doc))]
-pub mod uikit;
+use objc2::{msg_send, rc::Retained};
+use objc2::{msg_send_id, ClassType};
+use objc2_foundation::{MainThreadMarker, NSObject, NSObjectProtocol};
+use objc2_quartz_core::{CALayer, CAMetalLayer};
+use std::ptr::NonNull;
 
 /// A wrapper around [`CAMetalLayer`].
 #[doc(alias = "CAMetalLayer")]
@@ -54,9 +152,15 @@ impl UnwindSafe for Layer {}
 impl RefUnwindSafe for Layer {}
 
 impl Layer {
-    /// Get a pointer to the underlying [`CAMetalLayer`]. The pointer is valid
-    /// for at least as long as the [`Layer`] is valid, but can be extended by
-    /// retaining it.
+    /// Get a pointer to the underlying [`CAMetalLayer`]. The pointer is valid for at least as long
+    /// as the [`Layer`] is valid, but can be extended by retaining it.
+    ///
+    /// You should usually not change general `CALayer` properties like `bounds`, `contentsScale`
+    /// and so on of this layer, but instead modify the layer that it was created from.
+    ///
+    /// You can safely modify `CAMetalLayer` properties like `drawableSize` to match your needs,
+    /// though beware that if it does not match the actual size of the layer, the contents will be
+    /// scaled.
     ///
     ///
     /// # Example
@@ -86,4 +190,194 @@ impl Layer {
     pub fn pre_existing(&self) -> bool {
         self.pre_existing
     }
+
+    /// Get or create a new `CAMetalLayer` from the given `CALayer`.
+    ///
+    /// If the given layer is a `CAMetalLayer`, this will simply return that layer. If not, a new
+    /// `CAMetalLayer` is created and inserted as a sublayer, and then configured such that it will
+    /// have the same bounds and scale factor as the given layer.
+    ///
+    ///
+    /// # Safety
+    ///
+    /// The given layer pointer must be a valid instance of `CALayer`.
+    ///
+    ///
+    /// # Examples
+    ///
+    /// Create a new layer from a `CAMetalLayer`.
+    ///
+    /// ```
+    /// use std::ptr::NonNull;
+    /// use objc2_quartz_core::CAMetalLayer;
+    /// use raw_window_metal::Layer;
+    ///
+    /// let layer = unsafe { CAMetalLayer::new() };
+    /// let ptr: NonNull<CAMetalLayer> = NonNull::from(&*layer);
+    ///
+    /// let layer = unsafe { Layer::from_layer(ptr.cast()) };
+    /// assert!(layer.pre_existing());
+    /// ```
+    ///
+    /// Create a `CAMetalLayer` sublayer in a `CALayer`.
+    ///
+    /// ```
+    /// use std::ptr::NonNull;
+    /// use objc2_quartz_core::CALayer;
+    /// use raw_window_metal::Layer;
+    ///
+    /// let layer = CALayer::new();
+    /// let ptr: NonNull<CALayer> = NonNull::from(&*layer);
+    ///
+    /// let layer = unsafe { Layer::from_layer(ptr.cast()) };
+    /// assert!(!layer.pre_existing());
+    /// ```
+    pub unsafe fn from_layer(layer_ptr: NonNull<c_void>) -> Self {
+        // SAFETY: Caller ensures that the pointer is a valid `CALayer`.
+        let root_layer: &CALayer = unsafe { layer_ptr.cast().as_ref() };
+
+        // Check if the view's layer is already a `CAMetalLayer`.
+        if root_layer.is_kind_of::<CAMetalLayer>() {
+            let layer = root_layer.retain();
+            // SAFETY: Just checked that the layer is a `CAMetalLayer`.
+            let layer: Retained<CAMetalLayer> = unsafe { Retained::cast(layer) };
+            Layer {
+                layer,
+                pre_existing: true,
+            }
+        } else {
+            let layer = ObserverLayer::new(&root_layer);
+            Layer {
+                layer: Retained::into_super(layer),
+                pre_existing: false,
+            }
+        }
+    }
+
+    fn from_retained_layer(root_layer: Retained<CALayer>) -> Self {
+        // Check if the view's layer is already a `CAMetalLayer`.
+        if root_layer.is_kind_of::<CAMetalLayer>() {
+            // SAFETY: Just checked that the layer is a `CAMetalLayer`.
+            let layer: Retained<CAMetalLayer> = unsafe { Retained::cast(root_layer) };
+            Layer {
+                layer,
+                pre_existing: true,
+            }
+        } else {
+            let layer = ObserverLayer::new(&root_layer);
+            Layer {
+                layer: Retained::into_super(layer),
+                pre_existing: false,
+            }
+        }
+    }
+
+    /// Get or create a new `CAMetalLayer` from the given `NSView`.
+    ///
+    /// If the given view is not [layer-backed], it will be made so.
+    ///
+    /// If the given view has a `CAMetalLayer` as the root layer (which can happen for example if
+    /// the view has overwritten `-[NSView layerClass]` or the view is `MTKView`) this will simply
+    /// return that layer. If not, a new `CAMetalLayer` is created and inserted as a sublayer into
+    /// the view's layer, and then configured such that it will have the same bounds and scale
+    /// factor as the given view.
+    ///
+    ///
+    /// # Panics
+    ///
+    /// Panics if called from a thread that is not the main thread.
+    ///
+    ///
+    /// # Safety
+    ///
+    /// The given view pointer must be a valid instance of `NSView`.
+    ///
+    ///
+    /// # Example
+    ///
+    /// Construct a layer from an [`AppKitWindowHandle`].
+    ///
+    /// ```
+    /// use raw_window_handle::AppKitWindowHandle;
+    /// use raw_window_metal::Layer;
+    ///
+    /// let handle: AppKitWindowHandle;
+    /// # let mtm = objc2_foundation::MainThreadMarker::new().expect("doc tests to run on main thread");
+    /// # #[cfg(target_os = "macos")]
+    /// # let view = unsafe { objc2_app_kit::NSView::new(mtm) };
+    /// # #[cfg(target_os = "macos")]
+    /// # { handle = AppKitWindowHandle::new(std::ptr::NonNull::from(&*view).cast()) };
+    /// # #[cfg(not(target_os = "macos"))]
+    /// # { handle = unimplemented!() };
+    /// let layer = unsafe { Layer::from_ns_view(handle.ns_view) };
+    /// ```
+    ///
+    /// [layer-backed]: https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer?language=objc
+    /// [`AppKitWindowHandle`]: https://docs.rs/raw-window-handle/0.6.2/raw_window_handle/struct.AppKitWindowHandle.html
+    pub unsafe fn from_ns_view(ns_view_ptr: NonNull<c_void>) -> Self {
+        // let _mtm = MainThreadMarker::new().expect("can only access NSView on the main thread");
+
+        // SAFETY: Caller ensures that the pointer is a valid `NSView`.
+        //
+        // We use `NSObject` here to avoid importing `objc2-app-kit`.
+        let ns_view: &NSObject = unsafe { ns_view_ptr.cast().as_ref() };
+
+        // Force the view to become layer backed
+        let _: () = unsafe { msg_send![ns_view, setWantsLayer: true] };
+
+        // SAFETY: `-[NSView layer]` returns an optional `CALayer`
+        let root_layer: Option<Retained<CALayer>> = unsafe { msg_send_id![ns_view, layer] };
+        let root_layer = root_layer.expect("failed making the view layer-backed");
+
+        Self::from_retained_layer(root_layer)
+    }
+
+    /// Get or create a new `CAMetalLayer` from the given `UIView`.
+    ///
+    /// If the given view has a `CAMetalLayer` as the root layer (which can happen for example if
+    /// the view has overwritten `-[UIView layerClass]` or the view is `MTKView`) this will simply
+    /// return that layer. If not, a new `CAMetalLayer` is created and inserted as a sublayer into
+    /// the view's layer, and then configured such that it will have the same bounds and scale
+    /// factor as the given view.
+    ///
+    ///
+    /// # Panics
+    ///
+    /// Panics if called from a thread that is not the main thread.
+    ///
+    ///
+    /// # Safety
+    ///
+    /// The given view pointer must be a valid instance of `UIView`.
+    ///
+    ///
+    /// # Example
+    ///
+    /// Construct a layer from a [`UiKitWindowHandle`].
+    ///
+    /// ```no_run
+    /// use raw_window_handle::UiKitWindowHandle;
+    /// use raw_window_metal::Layer;
+    ///
+    /// let handle: UiKitWindowHandle;
+    /// # handle = unimplemented!();
+    /// let layer = unsafe { Layer::from_ui_view(handle.ui_view) };
+    /// ```
+    ///
+    /// [`UiKitWindowHandle`]: https://docs.rs/raw-window-handle/0.6.2/raw_window_handle/struct.UiKitWindowHandle.html
+    pub unsafe fn from_ui_view(ui_view_ptr: NonNull<c_void>) -> Self {
+        let _mtm = MainThreadMarker::new().expect("can only access UIView on the main thread");
+
+        // SAFETY: Caller ensures that the pointer is a valid `UIView`.
+        //
+        // We use `NSObject` here to avoid importing `objc2-ui-kit`.
+        let ui_view: &NSObject = unsafe { ui_view_ptr.cast().as_ref() };
+
+        // SAFETY: `-[UIView layer]` returns a non-optional `CALayer`
+        let root_layer: Retained<CALayer> = unsafe { msg_send_id![ui_view, layer] };
+
+        // Unlike on macOS, we cannot replace the main view as `UIView` does
+        // not allow it (when `NSView` does).
+        Self::from_retained_layer(root_layer)
+    }
 }
diff --git a/src/observer.rs b/src/observer.rs
new file mode 100644
index 0000000..ff8bea6
--- /dev/null
+++ b/src/observer.rs
@@ -0,0 +1,243 @@
+use core::ffi::c_void;
+use objc2::rc::{Retained, Weak};
+use objc2::runtime::{AnyClass, AnyObject};
+use objc2::{declare_class, msg_send, msg_send_id, mutability, ClassType, DeclaredClass};
+use objc2_foundation::{
+    ns_string, NSDictionary, NSKeyValueChangeKey, NSKeyValueChangeNewKey,
+    NSKeyValueObservingOptions, NSNumber, NSObjectNSKeyValueObserverRegistration, NSString,
+    NSValue,
+};
+use objc2_quartz_core::{CALayer, CAMetalLayer};
+
+declare_class!(
+    /// A `CAMetalLayer` layer that will automatically update its bounds and scale factor to match
+    /// its super layer.
+    ///
+    /// We do this by subclassing, to allow the user to just store the layer as
+    /// `Retained<CAMetalLayer>`, and still have our observers work.
+    pub(crate) struct ObserverLayer;
+
+    // SAFETY:
+    // - The superclass CAMetalLayer does not have any subclassing requirements.
+    // - Interior mutability is a safe default.
+    // - CustomLayer implements `Drop` and ensures that:
+    //   - It does not call an overridden method.
+    //   - It does not `retain` itself.
+    unsafe impl ClassType for ObserverLayer {
+        type Super = CAMetalLayer;
+        type Mutability = mutability::InteriorMutable;
+        const NAME: &'static str = "RawWindowMetalLayer";
+    }
+
+    impl DeclaredClass for ObserverLayer {
+        type Ivars = Weak<CALayer>;
+    }
+
+    // `NSKeyValueObserving` category.
+    //
+    // SAFETY: The method is correctly defined.
+    unsafe impl ObserverLayer {
+        #[method(observeValueForKeyPath:ofObject:change:context:)]
+        fn _observe_value(
+            &self,
+            key_path: Option<&NSString>,
+            object: Option<&AnyObject>,
+            change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
+            context: *mut c_void,
+        ) {
+            self.observe_value(key_path, object, change, context);
+        }
+    }
+);
+
+impl Drop for ObserverLayer {
+    fn drop(&mut self) {
+        // It is possible for the root layer to be de-allocated before the custom layer.
+        //
+        // In that case, the observer is already de-registered, and so we don't need to do anything.
+        //
+        // We use a weak variable here to avoid issues if the layer was removed from the super
+        // layer, and then later de-allocated, without de-registering these observers.
+        if let Some(root_layer) = self.ivars().load() {
+            unsafe {
+                root_layer.removeObserver_forKeyPath(self, ns_string!("contentsScale"));
+                root_layer.removeObserver_forKeyPath(self, ns_string!("bounds"));
+            }
+        }
+    }
+}
+
+impl ObserverLayer {
+    /// The context pointer, to differentiate between
+    fn context() -> *mut c_void {
+        ObserverLayer::class() as *const AnyClass as *mut c_void
+    }
+
+    /// Create a new custom layer that tracks parameters from the given super layer.
+    pub fn new(root_layer: &CALayer) -> Retained<Self> {
+        let this = Self::alloc().set_ivars(Weak::new(root_layer));
+        let this: Retained<Self> = unsafe { msg_send_id![super(this), init] };
+
+        // Add the layer as a sublayer of the root layer.
+        root_layer.addSublayer(&this);
+
+        // Do not use auto-resizing mask.
+        //
+        // This is done to work around a bug in macOS 14 and above, where views using auto layout
+        // may end up setting fractional values as the bounds, and that in turn doesn't propagate
+        // properly through the auto-resizing mask and with contents gravity.
+        //
+        // Instead, we keep the bounds of the layer in sync with the root layer using an observer,
+        // see below.
+        //
+        // this.setAutoresizingMask(kCALayerHeightSizable | kCALayerWidthSizable);
+
+        // AppKit / UIKit automatically sets the correct scale factor and bounds for layers attached
+        // to a view. Our layer, however, is not directly attached to a view, and so we need to
+        // observe changes to the root layer's parameters, and apply them to our layer.
+        //
+        // Note the use of `NSKeyValueObservingOptionInitial` to also set the initial values here.
+        //
+        // Note that for AppKit, we _could_ make the layer match the window by adding a delegate on
+        // the layer with the `layer:shouldInheritContentsScale:fromWindow:` method returning `true`
+        // - this tells the system to automatically update the scale factor when it changes on a
+        // window. But this wouldn't support headless rendering very well, and doesn't work on UIKit
+        // anyhow, so we might as well just always use the observer technique.
+        //
+        // SAFETY: Observer deregistered in `Drop` before the observer object is deallocated.
+        unsafe {
+            root_layer.addObserver_forKeyPath_options_context(
+                &this,
+                ns_string!("contentsScale"),
+                NSKeyValueObservingOptions::NSKeyValueObservingOptionNew
+                    | NSKeyValueObservingOptions::NSKeyValueObservingOptionInitial,
+                ObserverLayer::context(),
+            );
+            root_layer.addObserver_forKeyPath_options_context(
+                &this,
+                ns_string!("bounds"),
+                NSKeyValueObservingOptions::NSKeyValueObservingOptionNew
+                    | NSKeyValueObservingOptions::NSKeyValueObservingOptionInitial,
+                ObserverLayer::context(),
+            );
+        }
+
+        // The default content gravity (`kCAGravityResize`) is a fine choice for most applications,
+        // as it masks / alleviates issues with resizing and behaves better when moving the window
+        // between monitors, so we won't modify that.
+        //
+        // Unfortunately, it may also make it harder to debug resize issues, swap this for
+        // `kCAGravityTopLeft` instead when doing so.
+        //
+        // this.setContentsGravity(unsafe { kCAGravityResize });
+
+        this
+    }
+
+    fn observe_value(
+        &self,
+        key_path: Option<&NSString>,
+        object: Option<&AnyObject>,
+        change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
+        context: *mut c_void,
+    ) {
+        // An unrecognized context must belong to the super class.
+        if context != ObserverLayer::context() {
+            return unsafe {
+                msg_send![
+                    super(self),
+                    observeValueForKeyPath: key_path,
+                    ofObject: object,
+                    change: change,
+                    context: context,
+                ]
+            };
+        }
+
+        let change =
+            change.expect("requested a change dictionary in `addObserver`, but none was provided");
+        let new = change
+            .get(unsafe { NSKeyValueChangeNewKey })
+            .expect("requested change dictionary did not contain `NSKeyValueChangeNewKey`");
+
+        // NOTE: Setting these values usually causes a quarter second animation to occur, which is
+        // undesirable.
+        //
+        // However, since we're setting them inside an observer, there already is a transaction
+        // ongoing, and as such we don't need to wrap this in a `CATransaction` ourselves.
+
+        if key_path == Some(ns_string!("contentsScale")) {
+            let new = unsafe { &*(new as *const AnyObject as *const NSNumber) };
+            let scale_factor = new.as_cgfloat();
+
+            // Set the scale factor of the layer to match the root layer when it changes (e.g. if
+            // moved to a different monitor, or monitor settings changed).
+            self.setContentsScale(scale_factor);
+        } else if key_path == Some(ns_string!("bounds")) {
+            let new = unsafe { &*(new as *const AnyObject as *const NSValue) };
+            let bounds = new.get_rect().expect("new bounds value was not CGRect");
+
+            // Set `bounds` and `position` so that the new layer is inside the superlayer.
+            //
+            // This differs from just setting the `bounds`, as it also takes into account any
+            // translation that the superlayer may have that we'd want to preserve.
+            self.setFrame(bounds);
+        } else {
+            panic!("unknown observed keypath {key_path:?}");
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use objc2_foundation::{CGPoint, CGRect, CGSize};
+
+    use super::*;
+
+    #[test]
+    fn release_order_does_not_matter() {
+        let root_layer = CALayer::new();
+        let layer = ObserverLayer::new(&root_layer);
+        drop(root_layer);
+        drop(layer);
+
+        let root_layer = CALayer::new();
+        let layer = ObserverLayer::new(&root_layer);
+        drop(layer);
+        drop(root_layer);
+    }
+
+    #[test]
+    fn scale_factor_propagates() {
+        let root_layer = CALayer::new();
+        let layer = ObserverLayer::new(&root_layer);
+
+        root_layer.setContentsScale(3.0);
+        assert_eq!(layer.contentsScale(), 3.0);
+    }
+
+    #[test]
+    fn bounds_propagates() {
+        let root_layer = CALayer::new();
+        let layer = ObserverLayer::new(&root_layer);
+
+        root_layer.setBounds(CGRect::new(
+            CGPoint::new(10.0, 20.0),
+            CGSize::new(30.0, 40.0),
+        ));
+        assert_eq!(layer.position(), CGPoint::new(25.0, 40.0));
+        assert_eq!(
+            layer.bounds(),
+            CGRect::new(CGPoint::new(0.0, 0.0), CGSize::new(30.0, 40.0),)
+        );
+    }
+
+    #[test]
+    fn superlayer_can_remove_all_sublayers() {
+        let root_layer = CALayer::new();
+        let layer = ObserverLayer::new(&root_layer);
+        layer.removeFromSuperlayer();
+        drop(layer);
+        root_layer.setContentsScale(3.0);
+    }
+}
diff --git a/src/uikit.rs b/src/uikit.rs
deleted file mode 100644
index b48bd43..0000000
--- a/src/uikit.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-use crate::Layer;
-use objc2::rc::Retained;
-use objc2_foundation::NSObjectProtocol;
-use objc2_quartz_core::CAMetalLayer;
-use raw_window_handle::UiKitWindowHandle;
-use std::{ffi::c_void, ptr::NonNull};
-
-/// Get or create a new [`Layer`] associated with the given
-/// [`UiKitWindowHandle`].
-///
-/// # Safety
-///
-/// The handle must be valid.
-pub unsafe fn metal_layer_from_handle(handle: UiKitWindowHandle) -> Layer {
-    if let Some(_ui_view_controller) = handle.ui_view_controller {
-        // TODO: ui_view_controller support
-    }
-    unsafe { metal_layer_from_ui_view(handle.ui_view) }
-}
-
-/// Get or create a new [`Layer`] associated with the given `UIView`.
-///
-/// # Safety
-///
-/// The view must be a valid instance of `UIView`.
-pub unsafe fn metal_layer_from_ui_view(view: NonNull<c_void>) -> Layer {
-    // SAFETY: Caller ensures that the view is a `UIView`.
-    let view = unsafe { view.cast::<objc2_ui_kit::UIView>().as_ref() };
-
-    let main_layer = view.layer();
-
-    // Check if the view's layer is already a `CAMetalLayer`.
-    let render_layer = if main_layer.is_kind_of::<CAMetalLayer>() {
-        // SAFETY: Just checked that the layer is a `CAMetalLayer`.
-        let layer = unsafe { Retained::cast::<CAMetalLayer>(main_layer) };
-        Layer {
-            layer,
-            pre_existing: true,
-        }
-    } else {
-        // If the main layer is not a `CAMetalLayer`, we create a
-        // `CAMetalLayer` sublayer and use it instead.
-        //
-        // Unlike on macOS, we cannot replace the main view as `UIView` does
-        // not allow it (when `NSView` does).
-        let layer = unsafe { CAMetalLayer::new() };
-
-        let bounds = main_layer.bounds();
-        layer.setFrame(bounds);
-
-        main_layer.addSublayer(&layer);
-
-        Layer {
-            layer,
-            pre_existing: false,
-        }
-    };
-
-    if let Some(window) = view.window() {
-        view.setContentScaleFactor(window.screen().nativeScale());
-    }
-
-    render_layer
-}