Repository: tauri-apps/wry (language: rust)

Issue:
Incorrect and incosistent window size with multiwebview
**Describe the bug**
A clear and concise description of what the bug is.

**Steps To Reproduce**
```console
git clone https://github.com/tauri-apps/wry
cd wry
cargo run --example multiwebview
```



**Expected behavior**
The sizes should be of the whole window and same as it is in MacOS

**Screenshots**

## Windows (incorrect size)

<img width="600" alt="image" src="https://github.com/tauri-apps/wry/assets/61390950/b71a6333-c462-4606-a179-b439afe227d6">

## MacOS (correct size)

<img width="600" alt="image" src="https://github.com/tauri-apps/wry/assets/61390950/48987d97-daee-49b8-b135-2709f700ac39">



**Platform and Versions (please complete the following information):**
```console
OS: Windows 11
Rustc: 
cargo 1.75.0 (1d8b05cdd 2023-11-20)
rustc 1.75.0 (82e1608df 2023-12-21)
```

**Additional context**
It works well on `MacOS`




Discussion / hints:
Thanks for reporting, I am aware of that issue and I am working on fixing it before next release.
You may be already aware of, but looks like the problem is that in windows the expected size is PysicalSize but in MacOS the expected size is LogicalSize (calculated with scale factor by default in multiwebview example)
So in windows, by removing the `to_logical` in both window setup and resize handler fixes it.

Yeah, I think we will need to pick one or another and do the conversions internally.

Fix patch:
diff --git a/.changes/objc2.md b/.changes/objc2.md
new file mode 100644
index 000000000..57397a964
--- /dev/null
+++ b/.changes/objc2.md
@@ -0,0 +1,5 @@
+---
+"wry": minor
+---
+
+Migrate to obj2.
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index ff81aa2d2..3a80812ef 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,4 +1,4 @@
-workspace = { }
+workspace = {}
 
 [package]
 name = "wry"
@@ -10,31 +10,31 @@ description = "Cross-platform WebView rendering library"
 readme = "README.md"
 repository = "https://github.com/tauri-apps/wry"
 documentation = "https://docs.rs/wry"
-categories = [ "gui" ]
-exclude = [ "/.changes", "/.github", "/audits", "/wry-logo.svg" ]
+categories = ["gui"]
+exclude = ["/.changes", "/.github", "/audits", "/wry-logo.svg"]
 
 [package.metadata.docs.rs]
 no-default-features = true
-features = [ "drag-drop", "protocol", "os-webview" ]
+features = ["drag-drop", "protocol", "os-webview"]
 targets = [
   "x86_64-unknown-linux-gnu",
   "x86_64-pc-windows-msvc",
-  "x86_64-apple-darwin"
+  "x86_64-apple-darwin",
 ]
-rustc-args = [ "--cfg", "docsrs" ]
-rustdoc-args = [ "--cfg", "docsrs" ]
+rustc-args = ["--cfg", "docsrs"]
+rustdoc-args = ["--cfg", "docsrs"]
 
 [features]
-default = [ "drag-drop", "objc-exception", "protocol", "os-webview" ]
-serde = [ "dpi/serde" ]
-objc-exception = [ "objc/exception" ]
-drag-drop = [ ]
-protocol = [ ]
-devtools = [ ]
-transparent = [ ]
-fullscreen = [ ]
-linux-body = [ "webkit2gtk/v2_40", "os-webview" ]
-mac-proxy = [ ]
+default = ["drag-drop", "objc-exception", "protocol", "os-webview"]
+serde = ["dpi/serde"]
+objc-exception = ["objc2/catch-all"]
+drag-drop = []
+protocol = []
+devtools = []
+transparent = []
+fullscreen = []
+linux-body = ["webkit2gtk/v2_40", "os-webview"]
+mac-proxy = []
 os-webview = [
   "javascriptcore-rs",
   "webkit2gtk",
@@ -42,21 +42,23 @@ os-webview = [
   "dep:gtk",
   "soup3",
   "x11-dl",
-  "gdkx11"
+  "gdkx11",
 ]
-tracing = [ "dep:tracing" ]
+tracing = ["dep:tracing"]
 
 [dependencies]
 tracing = { version = "0.1", optional = true }
 once_cell = "1"
 thiserror = "1.0"
 http = "1.1"
-raw-window-handle = { version = "0.6", features = [ "std" ] }
+raw-window-handle = { version = "0.6", features = ["std"] }
 dpi = "0.1"
 
 [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
-javascriptcore-rs = { version = "=1.1.2", features = [ "v2_28" ], optional = true }
-webkit2gtk = { version = "=2.0.1", features = [ "v2_38" ], optional = true }
+javascriptcore-rs = { version = "=1.1.2", features = [
+  "v2_28",
+], optional = true }
+webkit2gtk = { version = "=2.0.1", features = ["v2_38"], optional = true }
 webkit2gtk-sys = { version = "=2.0.1", optional = true }
 gtk = { version = "0.18", optional = true }
 soup3 = { version = "0.5", optional = true }
@@ -87,15 +89,79 @@ features = [
   "Win32_Globalization",
   "Win32_UI_HiDpi",
   "Win32_UI_Input",
-  "Win32_UI_Input_KeyboardAndMouse"
+  "Win32_UI_Input_KeyboardAndMouse",
 ]
 
 [target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies]
-block = "0.1"
-cocoa = "0.26"
-core-graphics = "0.24"
-objc = "0.2"
-objc_id = "0.1"
+block2 = "0.5"
+objc2 = "0.5"
+objc2-web-kit = { version = "0.2.0", features = [
+  "objc2-app-kit",
+  "block2",
+  "WKWebView",
+  "WKWebViewConfiguration",
+  "WKWebsiteDataStore",
+  "WKDownload",
+  "WKDownloadDelegate",
+  "WKNavigation",
+  "WKNavigationDelegate",
+  "WKUserContentController",
+  "WKURLSchemeHandler",
+  "WKPreferences",
+  "WKURLSchemeTask",
+  "WKScriptMessageHandler",
+  "WKUIDelegate",
+  "WKOpenPanelParameters",
+  "WKFrameInfo",
+  "WKSecurityOrigin",
+  "WKScriptMessage",
+  "WKNavigationAction",
+  "WKWebpagePreferences",
+  "WKNavigationResponse",
+  "WKUserScript",
+] }
+objc2-foundation = { version = "0.2.0", features = [
+  "NSURLRequest",
+  "NSURL",
+  "NSString",
+  "NSKeyValueCoding",
+  "NSStream",
+  "NSDictionary",
+  "NSObject",
+  "NSData",
+  "NSKeyValueObserving",
+  "NSThread",
+  "NSJSONSerialization",
+  "NSDate",
+  "NSBundle",
+  "NSProcessInfo",
+  "NSValue",
+  "NSRange",
+] }
+
+[target."cfg(target_os = \"ios\")".dependencies]
+objc2-ui-kit = { version = "0.2.2", features = [
+  "UIResponder",
+  "UIScrollView",
+  "UIView",
+  "UIWindow",
+  "UIApplication",
+  "UIEvent",
+] }
+
+[target."cfg(target_os = \"macos\")".dependencies]
+objc2-app-kit = { version = "0.2.0", features = [
+  "NSApplication",
+  "NSEvent",
+  "NSWindow",
+  "NSView",
+  "NSPasteboard",
+  "NSPanel",
+  "NSResponder",
+  "NSOpenPanel",
+  "NSSavePanel",
+  "NSMenu",
+] }
 
 [target."cfg(target_os = \"android\")".dependencies]
 crossbeam-channel = "0.5"
@@ -119,4 +185,4 @@ percent-encoding = "2.3"
 
 [lints.rust.unexpected_cfgs]
 level = "warn"
-check-cfg = [ "cfg(linux)", "cfg(gtk)" ]
+check-cfg = ["cfg(linux)", "cfg(gtk)"]
diff --git a/examples/reparent.rs b/examples/reparent.rs
index 7d682a428..1b5476508 100644
--- a/examples/reparent.rs
+++ b/examples/reparent.rs
@@ -11,7 +11,7 @@ use tao::{
 use wry::WebViewBuilder;
 
 #[cfg(target_os = "macos")]
-use {tao::platform::macos::WindowExtMacOS, wry::WebViewExtMacOS};
+use {objc2_app_kit::NSWindow, tao::platform::macos::WindowExtMacOS, wry::WebViewExtMacOS};
 #[cfg(target_os = "windows")]
 use {tao::platform::windows::WindowExtWindows, wry::WebViewExtWindows};
 
@@ -91,7 +91,7 @@ fn main() -> wry::Result<()> {
 
         #[cfg(target_os = "macos")]
         webview
-          .reparent(new_parent.ns_window() as cocoa::base::id)
+          .reparent(new_parent.ns_window() as *mut NSWindow)
           .unwrap();
         #[cfg(not(any(
           target_os = "windows",
diff --git a/src/error.rs b/src/error.rs
index e74fb283d..e3b026117 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -55,6 +55,8 @@ pub enum Error {
   #[cfg(target_os = "android")]
   #[error(transparent)]
   CrossBeamRecvError(#[from] crossbeam_channel::RecvError),
+  #[error("not on the main thread")]
+  NotMainThread,
   #[error("Custom protocol task is invalid.")]
   CustomProtocolTaskInvalid,
   #[error("Failed to register URL scheme: {0}, could be due to invalid URL scheme or the scheme is already registered.")]
diff --git a/src/lib.rs b/src/lib.rs
index ccced7c19..dbd40d009 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -190,9 +190,9 @@
 #![allow(clippy::type_complexity)]
 #![cfg_attr(docsrs, feature(doc_cfg))]
 
-#[cfg(any(target_os = "macos", target_os = "ios"))]
-#[macro_use]
-extern crate objc;
+// #[cfg(any(target_os = "macos", target_os = "ios"))]
+// #[macro_use]
+// extern crate objc;
 
 mod error;
 mod proxy;
@@ -222,12 +222,18 @@ use raw_window_handle::HasWindowHandle;
 #[cfg(gtk)]
 use webkitgtk::*;
 
+#[cfg(any(target_os = "macos", target_os = "ios"))]
+use objc2::rc::Retained;
+#[cfg(target_os = "macos")]
+use objc2_app_kit::NSWindow;
+#[cfg(any(target_os = "macos", target_os = "ios"))]
+use objc2_web_kit::WKUserContentController;
 #[cfg(any(target_os = "macos", target_os = "ios"))]
 pub(crate) mod wkwebview;
 #[cfg(any(target_os = "macos", target_os = "ios"))]
 use wkwebview::*;
 #[cfg(any(target_os = "macos", target_os = "ios"))]
-pub use wkwebview::{PrintMargin, PrintOptions};
+pub use wkwebview::{PrintMargin, PrintOptions, WryWebView};
 
 #[cfg(target_os = "windows")]
 pub(crate) mod webview2;
@@ -416,7 +422,7 @@ pub struct WebViewAttributes<'a> {
   /// second is a mutable `PathBuf` reference that (possibly) represents where the file will be downloaded to. The latter
   /// parameter can be used to set the download location by assigning a new path to it, the assigned path _must_ be
   /// absolute. The closure returns a `bool` to allow or deny the download.
-  pub download_started_handler: Option<Box<dyn FnMut(String, &mut PathBuf) -> bool>>,
+  pub download_started_handler: Option<Box<dyn FnMut(String, &mut PathBuf) -> bool + 'static>>,
 
   /// A download completion handler to manage downloads that have finished.
   ///
@@ -1146,20 +1152,11 @@ impl<'a> WebViewBuilder<'a> {
 }
 
 #[cfg(any(target_os = "macos", target_os = "ios",))]
-#[derive(Clone)]
+#[derive(Clone, Default)]
 pub(crate) struct PlatformSpecificWebViewAttributes {
   data_store_identifier: Option<[u8; 16]>,
 }
 
-#[cfg(any(target_os = "macos", target_os = "ios",))]
-impl Default for PlatformSpecificWebViewAttributes {
-  fn default() -> Self {
-    Self {
-      data_store_identifier: None,
-    }
-  }
-}
-
 #[cfg(any(target_os = "macos", target_os = "ios",))]
 pub trait WebViewBuilderExtDarwin {
   /// Initialize the WebView with a custom data store identifier.
@@ -1767,35 +1764,32 @@ impl WebViewExtUnix for WebView {
 #[cfg(target_os = "macos")]
 pub trait WebViewExtMacOS {
   /// Returns WKWebView handle
-  fn webview(&self) -> cocoa::base::id;
+  fn webview(&self) -> Retained<WryWebView>;
   /// Returns WKWebView manager [(userContentController)](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler/1396222-usercontentcontroller) handle
-  fn manager(&self) -> cocoa::base::id;
+  fn manager(&self) -> Retained<WKUserContentController>;
   /// Returns NSWindow associated with the WKWebView webview
-  fn ns_window(&self) -> cocoa::base::id;
+  fn ns_window(&self) -> Retained<NSWindow>;
   /// Attaches this webview to the given NSWindow and removes it from the current one.
-  fn reparent(&self, window: cocoa::base::id) -> Result<()>;
+  fn reparent(&self, window: *mut NSWindow) -> Result<()>;
   // Prints with extra options
   fn print_with_options(&self, options: &PrintOptions) -> Result<()>;
 }
 
 #[cfg(target_os = "macos")]
 impl WebViewExtMacOS for WebView {
-  fn webview(&self) -> cocoa::base::id {
-    self.webview.webview
+  fn webview(&self) -> Retained<WryWebView> {
+    self.webview.webview.clone()
   }
 
-  fn manager(&self) -> cocoa::base::id {
-    self.webview.manager
+  fn manager(&self) -> Retained<WKUserContentController> {
+    self.webview.manager.clone()
   }
 
-  fn ns_window(&self) -> cocoa::base::id {
-    unsafe {
-      let ns_window: cocoa::base::id = msg_send![self.webview.webview, window];
-      ns_window
-    }
+  fn ns_window(&self) -> Retained<NSWindow> {
+    self.webview.webview.window().unwrap().clone()
   }
 
-  fn reparent(&self, window: cocoa::base::id) -> Result<()> {
+  fn reparent(&self, window: *mut NSWindow) -> Result<()> {
     self.webview.reparent(window)
   }
 
@@ -1808,19 +1802,19 @@ impl WebViewExtMacOS for WebView {
 #[cfg(target_os = "ios")]
 pub trait WebViewExtIOS {
   /// Returns WKWebView handle
-  fn webview(&self) -> cocoa::base::id;
+  fn webview(&self) -> Retained<WryWebView>;
   /// Returns WKWebView manager [(userContentController)](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler/1396222-usercontentcontroller) handle
-  fn manager(&self) -> cocoa::base::id;
+  fn manager(&self) -> Retained<WKUserContentController>;
 }
 
 #[cfg(target_os = "ios")]
 impl WebViewExtIOS for WebView {
-  fn webview(&self) -> cocoa::base::id {
-    self.webview.webview
+  fn webview(&self) -> Retained<WryWebView> {
+    self.webview.webview.clone()
   }
 
-  fn manager(&self) -> cocoa::base::id {
-    self.webview.manager
+  fn manager(&self) -> Retained<WKUserContentController> {
+    self.webview.manager.clone()
   }
 }
 
diff --git a/src/wkwebview/class/document_title_changed_observer.rs b/src/wkwebview/class/document_title_changed_observer.rs
new file mode 100644
index 000000000..90c6cdbfd
--- /dev/null
+++ b/src/wkwebview/class/document_title_changed_observer.rs
@@ -0,0 +1,98 @@
+// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{ffi::c_void, ptr::null_mut};
+
+use objc2::{
+  declare_class, msg_send, msg_send_id,
+  mutability::InteriorMutable,
+  rc::Retained,
+  runtime::{AnyObject, NSObject},
+  ClassType, DeclaredClass,
+};
+use objc2_foundation::{
+  NSDictionary, NSKeyValueChangeKey, NSKeyValueObservingOptions,
+  NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSString,
+};
+
+use crate::WryWebView;
+pub struct DocumentTitleChangedObserverIvars {
+  pub object: Retained<WryWebView>,
+  pub handler: Box<dyn Fn(String)>,
+}
+
+declare_class!(
+  pub struct DocumentTitleChangedObserver;
+
+  unsafe impl ClassType for DocumentTitleChangedObserver {
+    type Super = NSObject;
+    type Mutability = InteriorMutable;
+    const NAME: &'static str = "DocumentTitleChangedObserver";
+  }
+
+  impl DeclaredClass for DocumentTitleChangedObserver {
+    type Ivars = DocumentTitleChangedObserverIvars;
+  }
+
+  unsafe impl DocumentTitleChangedObserver {
+    #[method(observeValueForKeyPath:ofObject:change:context:)]
+    fn observe_value_for_key_path(
+      &self,
+      key_path: Option<&NSString>,
+      of_object: Option<&AnyObject>,
+      _change: Option<&NSDictionary<NSKeyValueChangeKey, AnyObject>>,
+      _context: *mut c_void,
+    ) {
+      if let (Some(key_path), Some(object)) = (key_path, of_object) {
+        if key_path.to_string() == "title" {
+          unsafe {
+            let handler = &self.ivars().handler;
+            // if !handler.is_null() {
+              let title: *const NSString = msg_send![object, title];
+              handler((*title).to_string());
+            // }
+          }
+        }
+      }
+    }
+  }
+
+  unsafe impl NSObjectProtocol for DocumentTitleChangedObserver {}
+);
+
+impl DocumentTitleChangedObserver {
+  pub fn new(webview: Retained<WryWebView>, handler: Box<dyn Fn(String)>) -> Retained<Self> {
+    let observer = Self::alloc().set_ivars(DocumentTitleChangedObserverIvars {
+      object: webview,
+      handler,
+    });
+
+    let observer: Retained<Self> = unsafe { msg_send_id![super(observer), init] };
+
+    unsafe {
+      observer
+        .ivars()
+        .object
+        .addObserver_forKeyPath_options_context(
+          &observer,
+          &NSString::from_str("title"),
+          NSKeyValueObservingOptions::NSKeyValueObservingOptionNew,
+          null_mut(),
+        );
+    }
+
+    observer
+  }
+}
+
+impl Drop for DocumentTitleChangedObserver {
+  fn drop(&mut self) {
+    unsafe {
+      self
+        .ivars()
+        .object
+        .removeObserver_forKeyPath(self, &NSString::from_str("title"));
+    }
+  }
+}
diff --git a/src/wkwebview/class/mod.rs b/src/wkwebview/class/mod.rs
new file mode 100644
index 000000000..22bd404ef
--- /dev/null
+++ b/src/wkwebview/class/mod.rs
@@ -0,0 +1,12 @@
+// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+pub mod document_title_changed_observer;
+pub mod url_scheme_handler;
+pub mod wry_download_delegate;
+pub mod wry_navigation_delegate;
+pub mod wry_web_view;
+pub mod wry_web_view_delegate;
+pub mod wry_web_view_parent;
+pub mod wry_web_view_ui_delegate;
diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs
new file mode 100644
index 000000000..b0aaf8937
--- /dev/null
+++ b/src/wkwebview/class/url_scheme_handler.rs
@@ -0,0 +1,304 @@
+// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{
+  borrow::Cow,
+  ffi::{c_char, c_void, CStr},
+  panic::AssertUnwindSafe,
+  ptr::NonNull,
+  slice,
+};
+
+use http::{
+  header::{CONTENT_LENGTH, CONTENT_TYPE},
+  Request, Response as HttpResponse, StatusCode, Version,
+};
+use objc2::{
+  rc::Retained,
+  runtime::{AnyClass, AnyObject, ClassBuilder, ProtocolObject},
+  ClassType,
+};
+use objc2_foundation::{
+  NSData, NSHTTPURLResponse, NSMutableDictionary, NSObject, NSObjectProtocol, NSString, NSURL,
+  NSUUID,
+};
+use objc2_web_kit::{WKURLSchemeHandler, WKURLSchemeTask};
+
+use crate::{wkwebview::WEBVIEW_IDS, RequestAsyncResponder, WryWebView};
+
+pub fn create(name: &str) -> &AnyClass {
+  unsafe {
+    let scheme_name = format!("{}URLSchemeHandler", name);
+    let cls = ClassBuilder::new(&scheme_name, NSObject::class());
+    match cls {
+      Some(mut cls) => {
+        cls.add_ivar::<*mut c_void>("function");
+        cls.add_ivar::<*mut c_char>("webview_id");
+        cls.add_method(
+          objc2::sel!(webView:startURLSchemeTask:),
+          start_task as extern "C" fn(_, _, _, _),
+        );
+        cls.add_method(
+          objc2::sel!(webView:stopURLSchemeTask:),
+          stop_task as extern "C" fn(_, _, _, _),
+        );
+        cls.register()
+      }
+      None => AnyClass::get(&scheme_name).expect("Failed to get the class definition"),
+    }
+  }
+}
+
+// Task handler for custom protocol
+extern "C" fn start_task(
+  this: &AnyObject,
+  _sel: objc2::runtime::Sel,
+  webview: &'static mut WryWebView,
+  task: &'static ProtocolObject<dyn WKURLSchemeTask>,
+) {
+  unsafe {
+    #[cfg(feature = "tracing")]
+          let span = tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty)
+            .entered();
+
+    let task_key = task.hash(); // hash by task object address
+    let task_uuid = webview.add_custom_task_key(task_key);
+
+    let ivar = this.class().instance_variable("webview_id").unwrap();
+    let webview_id_ptr: *mut c_char = *ivar.load(this);
+    let webview_id = CStr::from_ptr(webview_id_ptr)
+      .to_str()
+      .ok()
+      .unwrap_or_default();
+
+    let ivar = this.class().instance_variable("function").unwrap();
+    let function: &*mut c_void = ivar.load(this);
+    if !function.is_null() {
+      let function = &mut *(*function
+        as *mut Box<dyn Fn(crate::WebViewId, Request<Vec<u8>>, RequestAsyncResponder)>);
+
+      // Get url request
+      let request = task.request();
+      let url = request.URL().unwrap();
+
+      let uri = url.absoluteString().unwrap().to_string();
+
+      #[cfg(feature = "tracing")]
+      span.record("uri", uri.clone());
+
+      // Get request method (GET, POST, PUT etc...)
+      let method = request.HTTPMethod().unwrap().to_string();
+
+      // Prepare our HttpRequest
+      let mut http_request = Request::builder().uri(uri).method(method.as_str());
+
+      // Get body
+      let mut sent_form_body = Vec::new();
+      let body = request.HTTPBody();
+      let body_stream = request.HTTPBodyStream();
+      if let Some(body) = body {
+        let length = body.length();
+        let data_bytes = body.bytes();
+        sent_form_body = slice::from_raw_parts(data_bytes.as_ptr(), length).to_vec();
+      } else if let Some(body_stream) = body_stream {
+        body_stream.open();
+
+        while body_stream.hasBytesAvailable() {
+          sent_form_body.reserve(128);
+          let p = sent_form_body.as_mut_ptr().add(sent_form_body.len());
+          let read_length = sent_form_body.capacity() - sent_form_body.len();
+          let count = body_stream.read_maxLength(NonNull::new(p).unwrap(), read_length);
+          sent_form_body.set_len(sent_form_body.len() + count as usize);
+        }
+
+        body_stream.close();
+      }
+
+      // Extract all headers fields
+      let all_headers = request.allHTTPHeaderFields();
+
+      // get all our headers values and inject them in our request
+      if let Some(all_headers) = all_headers {
+        for current_header in all_headers.allKeys().to_vec() {
+          let header_value = all_headers.valueForKey(current_header).unwrap();
+
+          // inject the header into the request
+          http_request = http_request.header(current_header.to_string(), header_value.to_string());
+        }
+      }
+
+      let respond_with_404 = || {
+        let urlresponse = NSHTTPURLResponse::alloc();
+        let response = NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields(
+          urlresponse,
+          &url,
+          StatusCode::NOT_FOUND.as_u16().try_into().unwrap(),
+          Some(&NSString::from_str(
+            format!("{:#?}", Version::HTTP_11).as_str(),
+          )),
+          None,
+        )
+        .unwrap();
+        task.didReceiveResponse(&response);
+        // Finish
+        task.didFinish();
+      };
+
+      // send response
+      match http_request.body(sent_form_body) {
+        Ok(final_request) => {
+          let responder: Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)> =
+            Box::new(move |sent_response| {
+              fn check_webview_id_valid(webview_id: &str) -> crate::Result<()> {
+                if !WEBVIEW_IDS.lock().unwrap().contains(webview_id) {
+                  return Err(crate::Error::CustomProtocolTaskInvalid);
+                }
+                Ok(())
+              }
+              /// Task may not live longer than async custom protocol handler.
+              ///
+              /// There are roughly 2 ways to cause segfault:
+              /// 1. Task has stopped. pointer of the task not valid anymore.
+              /// 2. Task had stopped, but the pointer of the task has allocated to a new task.
+              ///    Outdated custom handler may call to the new task instance and cause segfault.
+              fn check_task_is_valid(
+                webview: &WryWebView,
+                task_key: usize,
+                current_uuid: Retained<NSUUID>,
+              ) -> crate::Result<()> {
+                let latest_task_uuid = webview.get_custom_task_uuid(task_key);
+                if let Some(latest_uuid) = latest_task_uuid {
+                  if latest_uuid != current_uuid {
+                    return Err(crate::Error::CustomProtocolTaskInvalid);
+                  }
+                } else {
+                  return Err(crate::Error::CustomProtocolTaskInvalid);
+                }
+                Ok(())
+              }
+
+              unsafe fn response(
+                // FIXME: though we give it a static lifetime, it's not guaranteed to be valid.
+                task: &'static ProtocolObject<dyn WKURLSchemeTask>,
+                // FIXME: though we give it a static lifetime, it's not guaranteed to be valid.
+                webview: &'static mut WryWebView,
+                task_key: usize,
+                task_uuid: Retained<NSUUID>,
+                webview_id: &str,
+                url: Retained<NSURL>,
+                sent_response: HttpResponse<Cow<'_, [u8]>>,
+              ) -> crate::Result<()> {
+                check_task_is_valid(&*webview, task_key, task_uuid.clone())?;
+
+                let content = sent_response.body();
+                // default: application/octet-stream, but should be provided by the client
+                let wanted_mime = sent_response.headers().get(CONTENT_TYPE);
+                // default to 200
+                let wanted_status_code = sent_response.status().as_u16() as i32;
+                // default to HTTP/1.1
+                let wanted_version = format!("{:#?}", sent_response.version());
+
+                let mut headers = NSMutableDictionary::new();
+
+                if let Some(mime) = wanted_mime {
+                  headers.insert_id(
+                    NSString::from_str(mime.to_str().unwrap()).as_ref(),
+                    NSString::from_str(CONTENT_TYPE.as_str()),
+                  );
+                }
+                headers.insert_id(
+                  NSString::from_str(&content.len().to_string()).as_ref(),
+                  NSString::from_str(CONTENT_LENGTH.as_str()),
+                );
+
+                // add headers
+                for (name, value) in sent_response.headers().iter() {
+                  if let Ok(value) = value.to_str() {
+                    headers.insert_id(
+                      NSString::from_str(name.as_str()).as_ref(),
+                      NSString::from_str(value),
+                    );
+                  }
+                }
+
+                let urlresponse = NSHTTPURLResponse::alloc();
+                let response = NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields(
+                  urlresponse,
+                  &url,
+                  wanted_status_code.try_into().unwrap(),
+                  Some(&NSString::from_str(&wanted_version)),
+                  Some(&headers),
+                )
+                .unwrap();
+
+                check_webview_id_valid(webview_id)?;
+                check_task_is_valid(&*webview, task_key, task_uuid.clone())?;
+
+                objc2::exception::catch(AssertUnwindSafe(|| {
+                  task.didReceiveResponse(&response);
+                }))
+                .unwrap();
+
+                // Send data
+                let bytes = content.as_ptr() as *mut c_void;
+                let data = NSData::alloc();
+                // MIGRATE NOTE: we copied the content to the NSData because content will be freed
+                // when out of scope but NSData will also free the content when it's done and cause doube free.
+                let data = NSData::initWithBytes_length(data, bytes, content.len());
+                check_webview_id_valid(webview_id)?;
+                check_task_is_valid(&*webview, task_key, task_uuid.clone())?;
+                objc2::exception::catch(AssertUnwindSafe(|| {
+                  task.didReceiveData(&data);
+                }))
+                .unwrap();
+
+                // Finish
+                check_webview_id_valid(webview_id)?;
+                check_task_is_valid(&*webview, task_key, task_uuid.clone())?;
+                objc2::exception::catch(AssertUnwindSafe(|| {
+                  task.didFinish();
+                }))
+                .unwrap();
+
+                webview.remove_custom_task_key(task_key);
+                Ok(())
+              }
+
+              let _ = response(
+                task,
+                webview,
+                task_key,
+                task_uuid,
+                webview_id,
+                url.clone(),
+                sent_response,
+              );
+            });
+
+          #[cfg(feature = "tracing")]
+          let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
+          function(
+            webview_id,
+            final_request,
+            RequestAsyncResponder { responder },
+          );
+        }
+        Err(_) => respond_with_404(),
+      };
+    } else {
+      #[cfg(feature = "tracing")]
+      tracing::warn!(
+        "Either WebView or WebContext instance is dropped! This handler shouldn't be called."
+      );
+    }
+  }
+}
+extern "C" fn stop_task(
+  _this: &ProtocolObject<dyn WKURLSchemeHandler>,
+  _sel: objc2::runtime::Sel,
+  webview: &mut WryWebView,
+  task: &ProtocolObject<dyn WKURLSchemeTask>,
+) {
+  webview.remove_custom_task_key(task.hash());
+}
diff --git a/src/wkwebview/class/wry_download_delegate.rs b/src/wkwebview/class/wry_download_delegate.rs
new file mode 100644
index 000000000..5521b6509
--- /dev/null
+++ b/src/wkwebview/class/wry_download_delegate.rs
@@ -0,0 +1,82 @@
+// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{cell::RefCell, path::PathBuf, rc::Rc};
+
+use objc2::{
+  declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, runtime::NSObject,
+  ClassType, DeclaredClass,
+};
+use objc2_foundation::{
+  MainThreadMarker, NSData, NSError, NSObjectProtocol, NSString, NSURLResponse, NSURL,
+};
+use objc2_web_kit::{WKDownload, WKDownloadDelegate};
+
+use crate::wkwebview::download::{download_did_fail, download_did_finish, download_policy};
+
+pub struct WryDownloadDelegateIvars {
+  pub started: Option<RefCell<Box<dyn FnMut(String, &mut PathBuf) -> bool + 'static>>>,
+  pub completed: Option<Rc<dyn Fn(String, Option<PathBuf>, bool) + 'static>>,
+}
+
+declare_class!(
+  pub struct WryDownloadDelegate;
+
+  unsafe impl ClassType for WryDownloadDelegate {
+    type Super = NSObject;
+    type Mutability = MainThreadOnly;
+    const NAME: &'static str = "WryDownloadDelegate";
+  }
+
+  impl DeclaredClass for WryDownloadDelegate {
+    type Ivars = WryDownloadDelegateIvars;
+  }
+
+  unsafe impl NSObjectProtocol for WryDownloadDelegate {}
+
+  unsafe impl WKDownloadDelegate for WryDownloadDelegate {
+    #[method(download:decideDestinationUsingResponse:suggestedFilename:completionHandler:)]
+    fn download_policy(
+      &self,
+      download: &WKDownload,
+      response: &NSURLResponse,
+      suggested_path: &NSString,
+      handler: &block2::Block<dyn Fn(*const NSURL)>,
+    ) {
+      download_policy(self, download, response, suggested_path, handler);
+    }
+
+    #[method(downloadDidFinish:)]
+    fn download_did_finish(&self, download: &WKDownload) {
+      download_did_finish(self, download);
+    }
+
+    #[method(download:didFailWithError:resumeData:)]
+    fn download_did_fail(
+      &self,
+      download: &WKDownload,
+      error: &NSError,
+      resume_data: &NSData,
+    ) {
+      download_did_fail(self, download, error, resume_data);
+    }
+  }
+);
+
+impl WryDownloadDelegate {
+  pub fn new(
+    download_started_handler: Option<Box<dyn FnMut(String, &mut PathBuf) -> bool + 'static>>,
+    download_completed_handler: Option<Rc<dyn Fn(String, Option<PathBuf>, bool) + 'static>>,
+    mtm: MainThreadMarker,
+  ) -> Retained<Self> {
+    let delegate = mtm
+      .alloc::<WryDownloadDelegate>()
+      .set_ivars(WryDownloadDelegateIvars {
+        started: download_started_handler.map(|handler| RefCell::new(handler)),
+        completed: download_completed_handler,
+      });
+
+    unsafe { msg_send_id![super(delegate), init] }
+  }
+}
diff --git a/src/wkwebview/class/wry_navigation_delegate.rs b/src/wkwebview/class/wry_navigation_delegate.rs
new file mode 100644
index 000000000..82cc67a74
--- /dev/null
+++ b/src/wkwebview/class/wry_navigation_delegate.rs
@@ -0,0 +1,164 @@
+// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::sync::{Arc, Mutex};
+
+use objc2::{
+  declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, runtime::NSObject,
+  ClassType, DeclaredClass,
+};
+use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
+use objc2_web_kit::{
+  WKDownload, WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationDelegate,
+  WKNavigationResponse, WKNavigationResponsePolicy,
+};
+
+#[cfg(target_os = "ios")]
+use crate::wkwebview::ios::WKWebView::WKWebView;
+#[cfg(target_os = "macos")]
+use objc2_web_kit::WKWebView;
+
+use crate::{
+  url_from_webview,
+  wkwebview::{
+    download::{navigation_download_action, navigation_download_response},
+    navigation::{
+      did_commit_navigation, did_finish_navigation, navigation_policy, navigation_policy_response,
+    },
+  },
+  PageLoadEvent, WryWebView,
+};
+
+use super::wry_download_delegate::WryDownloadDelegate;
+
+pub struct WryNavigationDelegateIvars {
+  pub pending_scripts: Arc<Mutex<Option<Vec<String>>>>,
+  pub has_download_handler: bool,
+  pub navigation_policy_function: Box<dyn Fn(String, bool) -> bool>,
+  pub download_delegate: Option<Retained<WryDownloadDelegate>>,
+  pub on_page_load_handler: Option<Box<dyn Fn(PageLoadEvent)>>,
+}
+
+declare_class!(
+  pub struct WryNavigationDelegate;
+
+  unsafe impl ClassType for WryNavigationDelegate {
+    type Super = NSObject;
+    type Mutability = MainThreadOnly;
+    const NAME: &'static str = "WryNavigationDelegate";
+  }
+
+  impl DeclaredClass for WryNavigationDelegate {
+    type Ivars = WryNavigationDelegateIvars;
+  }
+
+  unsafe impl NSObjectProtocol for WryNavigationDelegate {}
+
+  unsafe impl WKNavigationDelegate for WryNavigationDelegate {
+    #[method(webView:decidePolicyForNavigationAction:decisionHandler:)]
+    fn navigation_policy(
+      &self,
+      webview: &WKWebView,
+      action: &WKNavigationAction,
+      handler: &block2::Block<dyn Fn(WKNavigationActionPolicy)>,
+    ) {
+      navigation_policy(self, webview, action, handler);
+    }
+
+    #[method(webView:decidePolicyForNavigationResponse:decisionHandler:)]
+    fn navigation_policy_response(
+      &self,
+      webview: &WKWebView,
+      response: &WKNavigationResponse,
+      handler: &block2::Block<dyn Fn(WKNavigationResponsePolicy)>,
+    ) {
+      navigation_policy_response(self, webview, response, handler);
+    }
+
+    #[method(webView:didFinishNavigation:)]
+    fn did_finish_navigation(
+      &self,
+      webview: &WKWebView,
+      navigation: &WKNavigation,
+    ) {
+      did_finish_navigation(self, webview, navigation);
+    }
+
+    #[method(webView:didCommitNavigation:)]
+    fn did_commit_navigation(
+      &self,
+      webview: &WKWebView,
+      navigation: &WKNavigation,
+    ) {
+      did_commit_navigation(self, webview, navigation);
+    }
+
+    #[method(webView:navigationAction:didBecomeDownload:)]
+    fn navigation_download_action(
+      &self,
+      webview: &WKWebView,
+      action: &WKNavigationAction,
+      download: &WKDownload,
+    ) {
+      navigation_download_action(self, webview, action, download);
+    }
+
+    #[method(webView:navigationResponse:didBecomeDownload:)]
+    fn navigation_download_response(
+      &self,
+      webview: &WKWebView,
+      response: &WKNavigationResponse,
+      download: &WKDownload,
+    ) {
+      navigation_download_response(self, webview, response, download);
+    }
+  }
+);
+
+impl WryNavigationDelegate {
+  #[allow(clippy::too_many_arguments)]
+  pub fn new(
+    webview: Retained<WryWebView>,
+    pending_scripts: Arc<Mutex<Option<Vec<String>>>>,
+    has_download_handler: bool,
+    navigation_handler: Option<Box<dyn Fn(String) -> bool>>,
+    new_window_req_handler: Option<Box<dyn Fn(String) -> bool>>,
+    download_delegate: Option<Retained<WryDownloadDelegate>>,
+    on_page_load_handler: Option<Box<dyn Fn(PageLoadEvent, String)>>,
+    mtm: MainThreadMarker,
+  ) -> Retained<Self> {
+    let navigation_policy_function = Box::new(move |url: String, is_main_frame: bool| -> bool {
+      if is_main_frame {
+        navigation_handler
+          .as_ref()
+          .map_or(true, |navigation_handler| (navigation_handler)(url))
+      } else {
+        new_window_req_handler
+          .as_ref()
+          .map_or(true, |new_window_req_handler| (new_window_req_handler)(url))
+      }
+    });
+
+    let on_page_load_handler = if let Some(handler) = on_page_load_handler {
+      let custom_handler = Box::new(move |event| {
+        handler(event, url_from_webview(&webview).unwrap_or_default());
+      }) as Box<dyn Fn(PageLoadEvent)>;
+      Some(custom_handler)
+    } else {
+      None
+    };
+
+    let delegate = mtm
+      .alloc::<WryNavigationDelegate>()
+      .set_ivars(WryNavigationDelegateIvars {
+        pending_scripts,
+        navigation_policy_function,
+        has_download_handler,
+        download_delegate,
+        on_page_load_handler,
+      });
+
+    unsafe { msg_send_id![super(delegate), init] }
+  }
+}
diff --git a/src/wkwebview/class/wry_web_view.rs b/src/wkwebview/class/wry_web_view.rs
new file mode 100644
index 000000000..0ebd7e762
--- /dev/null
+++ b/src/wkwebview/class/wry_web_view.rs
@@ -0,0 +1,154 @@
+// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::collections::HashMap;
+
+#[cfg(target_os = "macos")]
+use objc2::runtime::ProtocolObject;
+use objc2::{
+  declare_class, mutability::MainThreadOnly, rc::Retained, runtime::Bool, ClassType, DeclaredClass,
+};
+#[cfg(target_os = "macos")]
+use objc2_app_kit::{NSDraggingDestination, NSEvent};
+use objc2_foundation::{NSObjectProtocol, NSUUID};
+
+#[cfg(target_os = "ios")]
+use crate::wkwebview::ios::WKWebView::WKWebView;
+#[cfg(target_os = "macos")]
+use crate::{
+  wkwebview::{drag_drop, synthetic_mouse_events},
+  DragDropEvent,
+};
+#[cfg(target_os = "ios")]
+use objc2_ui_kit::UIEvent as NSEvent;
+#[cfg(target_os = "macos")]
+use objc2_web_kit::WKWebView;
+
+pub struct WryWebViewIvars {
+  pub(crate) is_child: bool,
+  #[cfg(target_os = "macos")]
+  pub(crate) drag_drop_handler: Box<dyn Fn(DragDropEvent) -> bool>,
+  #[cfg(target_os = "macos")]
+  pub(crate) accept_first_mouse: objc2::runtime::Bool,
+  pub(crate) custom_protocol_task_ids: HashMap<usize, Retained<NSUUID>>,
+}
+
+declare_class!(
+  pub struct WryWebView;
+
+  unsafe impl ClassType for WryWebView {
+    type Super = WKWebView;
+    type Mutability = MainThreadOnly;
+    const NAME: &'static str = "WryWebView";
+  }
+
+  impl DeclaredClass for WryWebView {
+    type Ivars = WryWebViewIvars;
+  }
+
+  unsafe impl WryWebView {
+    #[method(performKeyEquivalent:)]
+    fn perform_key_equivalent(
+      &self,
+      event: &NSEvent,
+    ) -> Bool {
+      // This is a temporary workaround for https://github.com/tauri-apps/tauri/issues/9426
+      // FIXME: When the webview is a child webview, performKeyEquivalent always return YES
+      // and stop propagating the event to the window, hence the menu shortcut won't be
+      // triggered. However, overriding this method also means the cmd+key event won't be
+      // handled in webview, which means the key cannot be listened by JavaScript.
+      if self.ivars().is_child {
+        Bool::NO
+      } else {
+        unsafe {
+          objc2::msg_send![super(self), performKeyEquivalent: event]
+        }
+      }
+    }
+
+    #[cfg(target_os = "macos")]
+    #[method(acceptsFirstMouse:)]
+    fn accept_first_mouse(
+      &self,
+      _event: &NSEvent,
+    ) -> Bool {
+        self.ivars().accept_first_mouse
+    }
+  }
+  unsafe impl NSObjectProtocol for WryWebView {}
+
+  // Drag & Drop
+  #[cfg(target_os = "macos")]
+  unsafe impl NSDraggingDestination for WryWebView {
+    #[method(draggingEntered:)]
+    fn dragging_entered(
+      &self,
+      drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>,
+    ) -> objc2_app_kit::NSDragOperation {
+      drag_drop::dragging_entered(self, drag_info)
+    }
+
+    #[method(draggingUpdated:)]
+    fn dragging_updated(
+      &self,
+      drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>,
+    ) -> objc2_app_kit::NSDragOperation {
+      drag_drop::dragging_updated(self, drag_info)
+    }
+
+    #[method(performDragOperation:)]
+    fn perform_drag_operation(
+      &self,
+      drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>,
+    ) -> Bool {
+      drag_drop::perform_drag_operation(self, drag_info)
+    }
+
+    #[method(draggingExited:)]
+    fn dragging_exited(
+      &self,
+      drag_info: &ProtocolObject<dyn objc2_app_kit::NSDraggingInfo>,
+    ) {
+      drag_drop::dragging_exited(self, drag_info)
+    }
+  }
+
+  // Synthetic mouse events
+  #[cfg(target_os = "macos")]
+  unsafe impl WryWebView {
+    #[method(otherMouseDown:)]
+    fn other_mouse_down(
+      &self,
+      event: &NSEvent,
+    ) {
+      synthetic_mouse_events::other_mouse_down(self, event)
+    }
+
+    #[method(otherMouseUp:)]
+    fn other_mouse_up(
+      &self,
+      event: &NSEvent,
+    ) {
+      synthetic_mouse_events::other_mouse_up(self, event)
+    }
+  }
+);
+
+// Custom Protocol Task Checker
+impl WryWebView {
+  pub(crate) fn add_custom_task_key(&mut self, task_id: usize) -> Retained<NSUUID> {
+    let task_uuid = NSUUID::new();
+    self
+      .ivars_mut()
+      .custom_protocol_task_ids
+      .insert(task_id, task_uuid.clone());
+    task_uuid
+  }
+  pub(crate) fn remove_custom_task_key(&mut self, task_id: usize) {
+    self.ivars_mut().custom_protocol_task_ids.remove(&task_id);
+  }
+  pub(crate) fn get_custom_task_uuid(&self, task_id: usize) -> Option<Retained<NSUUID>> {
+    self.ivars().custom_protocol_task_ids.get(&task_id).cloned()
+  }
+}
diff --git a/src/wkwebview/class/wry_web_view_delegate.rs b/src/wkwebview/class/wry_web_view_delegate.rs
new file mode 100644
index 000000000..d49fe00e5
--- /dev/null
+++ b/src/wkwebview/class/wry_web_view_delegate.rs
@@ -0,0 +1,108 @@
+// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::ffi::CStr;
+
+use http::Request;
+use objc2::{
+  declare_class, msg_send_id,
+  mutability::MainThreadOnly,
+  rc::Retained,
+  runtime::{NSObject, ProtocolObject},
+  ClassType, DeclaredClass,
+};
+use objc2_foundation::{MainThreadMarker, NSObjectProtocol, NSString};
+use objc2_web_kit::{WKScriptMessage, WKScriptMessageHandler, WKUserContentController};
+
+pub const IPC_MESSAGE_HANDLER_NAME: &str = "ipc";
+
+pub struct WryWebViewDelegateIvars {
+  pub controller: Retained<WKUserContentController>,
+  pub ipc_handler: Box<dyn Fn(Request<String>)>,
+}
+
+declare_class!(
+  pub struct WryWebViewDelegate;
+
+  unsafe impl ClassType for WryWebViewDelegate {
+    type Super = NSObject;
+    type Mutability = MainThreadOnly;
+    const NAME: &'static str = "WryWebViewDelegate";
+  }
+
+  impl DeclaredClass for WryWebViewDelegate {
+    type Ivars = WryWebViewDelegateIvars;
+  }
+
+  unsafe impl NSObjectProtocol for WryWebViewDelegate {}
+
+  unsafe impl WKScriptMessageHandler for WryWebViewDelegate {
+    // Function for ipc handler
+    #[method(userContentController:didReceiveScriptMessage:)]
+    fn did_receive(
+      this: &WryWebViewDelegate,
+      _controller: &WKUserContentController,
+      msg: &WKScriptMessage,
+    ) {
+      // Safety: objc runtime calls are unsafe
+      unsafe {
+        #[cfg(feature = "tracing")]
+        let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered();
+
+        let ipc_handler = &this.ivars().ipc_handler;
+        let body = msg.body();
+        let is_string = Retained::cast::<NSObject>(body.clone()).isKindOfClass(NSString::class());
+        if is_string {
+          let body = Retained::cast::<NSString>(body);
+          let js_utf8 = body.UTF8String();
+
+          let frame_info = msg.frameInfo();
+          let request = frame_info.request();
+          let url = request.URL().unwrap();
+          let absolute_url = url.absoluteString().unwrap();
+          let url_utf8 = absolute_url.UTF8String();
+
+          if let (Ok(url), Ok(js)) = (
+            CStr::from_ptr(url_utf8).to_str(),
+            CStr::from_ptr(js_utf8).to_str(),
+          ) {
+            ipc_handler(Request::builder().uri(url).body(js.to_string()).unwrap());
+            return;
+          }
+        }
+
+        #[cfg(feature = "tracing")]
+        tracing::warn!("WebView received invalid IPC call.");
+      }
+    }
+  }
+);
+
+impl WryWebViewDelegate {
+  pub fn new(
+    controller: Retained<WKUserContentController>,
+    ipc_handler: Box<dyn Fn(Request<String>)>,
+    mtm: MainThreadMarker,
+  ) -> Retained<Self> {
+    let delegate = mtm
+      .alloc::<WryWebViewDelegate>()
+      .set_ivars(WryWebViewDelegateIvars {
+        ipc_handler,
+        controller,
+      });
+
+    let delegate: Retained<Self> = unsafe { msg_send_id![super(delegate), init] };
+
+    let proto_delegate = ProtocolObject::from_ref(delegate.as_ref());
+    unsafe {
+      // this will increate the retain count of the delegate
+      delegate.ivars().controller.addScriptMessageHandler_name(
+        proto_delegate,
+        &NSString::from_str(IPC_MESSAGE_HANDLER_NAME),
+      );
+    }
+
+    delegate
+  }
+}
diff --git a/src/wkwebview/class/wry_web_view_parent.rs b/src/wkwebview/class/wry_web_view_parent.rs
new file mode 100644
index 000000000..95ef05c50
--- /dev/null
+++ b/src/wkwebview/class/wry_web_view_parent.rs
@@ -0,0 +1,55 @@
+// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use objc2::{
+  declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, ClassType, DeclaredClass,
+};
+#[cfg(target_os = "macos")]
+use objc2_app_kit::{NSApplication, NSEvent, NSView};
+use objc2_foundation::MainThreadMarker;
+#[cfg(target_os = "ios")]
+use objc2_ui_kit::UIView as NSView;
+
+pub struct WryWebViewParentIvars {}
+
+declare_class!(
+  pub struct WryWebViewParent;
+
+  unsafe impl ClassType for WryWebViewParent {
+    type Super = NSView;
+    type Mutability = MainThreadOnly;
+    const NAME: &'static str = "WryWebViewParent";
+  }
+
+  impl DeclaredClass for WryWebViewParent {
+    type Ivars = WryWebViewParentIvars;
+  }
+
+  unsafe impl WryWebViewParent {
+    #[cfg(target_os = "macos")]
+    #[method(keyDown:)]
+    fn key_down(
+      &self,
+      event: &NSEvent,
+    ) {
+      let mtm = MainThreadMarker::new().unwrap();
+      let app = NSApplication::sharedApplication(mtm);
+      unsafe {
+        if let Some(menu) = app.mainMenu() {
+          menu.performKeyEquivalent(event);
+        }
+      }
+    }
+  }
+);
+
+impl WryWebViewParent {
+  #[allow(dead_code)]
+  pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
+    let delegate = mtm
+      .alloc::<WryWebViewParent>()
+      .set_ivars(WryWebViewParentIvars {});
+    unsafe { msg_send_id![super(delegate), init] }
+  }
+}
diff --git a/src/wkwebview/class/wry_web_view_ui_delegate.rs b/src/wkwebview/class/wry_web_view_ui_delegate.rs
new file mode 100644
index 000000000..d436a29a4
--- /dev/null
+++ b/src/wkwebview/class/wry_web_view_ui_delegate.rs
@@ -0,0 +1,95 @@
+// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#[cfg(target_os = "macos")]
+use std::ptr::null_mut;
+
+use block2::Block;
+use objc2::{
+  declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, runtime::NSObject,
+  ClassType, DeclaredClass,
+};
+#[cfg(target_os = "macos")]
+use objc2_app_kit::{NSModalResponse, NSModalResponseOK, NSOpenPanel};
+use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
+#[cfg(target_os = "macos")]
+use objc2_foundation::{NSArray, NSURL};
+
+#[cfg(target_os = "macos")]
+use objc2_web_kit::WKOpenPanelParameters;
+use objc2_web_kit::{
+  WKFrameInfo, WKMediaCaptureType, WKPermissionDecision, WKSecurityOrigin, WKUIDelegate,
+};
+
+use crate::WryWebView;
+
+pub struct WryWebViewUIDelegateIvars {}
+
+declare_class!(
+  pub struct WryWebViewUIDelegate;
+
+  unsafe impl ClassType for WryWebViewUIDelegate {
+    type Super = NSObject;
+    type Mutability = MainThreadOnly;
+    const NAME: &'static str = "WryWebViewUIDelegate";
+  }
+
+  impl DeclaredClass for WryWebViewUIDelegate {
+    type Ivars = WryWebViewUIDelegateIvars;
+  }
+
+  unsafe impl NSObjectProtocol for WryWebViewUIDelegate {}
+
+  unsafe impl WKUIDelegate for WryWebViewUIDelegate {
+    #[cfg(target_os = "macos")]
+    #[method(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:)]
+    fn run_file_upload_panel(
+      &self,
+      _webview: &WryWebView,
+      open_panel_params: &WKOpenPanelParameters,
+      _frame: &WKFrameInfo,
+      handler: &block2::Block<dyn Fn(*const NSArray<NSURL>)>
+    ) {
+      unsafe {
+        if let Some(mtm) = MainThreadMarker::new() {
+          let open_panel = NSOpenPanel::openPanel(mtm);
+          open_panel.setCanChooseFiles(true);
+          let allow_multi = open_panel_params.allowsMultipleSelection();
+          open_panel.setAllowsMultipleSelection(allow_multi);
+          let allow_dir = open_panel_params.allowsDirectories();
+          open_panel.setCanChooseDirectories(allow_dir);
+          let ok: NSModalResponse = open_panel.runModal();
+          if ok == NSModalResponseOK {
+            let url = open_panel.URLs();
+            (*handler).call((Retained::as_ptr(&url),));
+          } else {
+            (*handler).call((null_mut(),));
+          }
+        }
+      }
+    }
+
+    #[method(webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:)]
+    fn request_media_capture_permission(
+      &self,
+      _webview: &WryWebView,
+      _origin: &WKSecurityOrigin,
+      _frame: &WKFrameInfo,
+      _capture_type: WKMediaCaptureType,
+      decision_handler: &Block<dyn Fn(WKPermissionDecision)>
+    ) {
+      //https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc
+      (*decision_handler).call((WKPermissionDecision::Grant,));
+    }
+  }
+);
+
+impl WryWebViewUIDelegate {
+  pub fn new(mtm: MainThreadMarker) -> Retained<Self> {
+    let delegate = mtm
+      .alloc::<WryWebViewUIDelegate>()
+      .set_ivars(WryWebViewUIDelegateIvars {});
+    unsafe { msg_send_id![super(delegate), init] }
+  }
+}
diff --git a/src/wkwebview/download.rs b/src/wkwebview/download.rs
index 4752e0b05..0f6724a62 100644
--- a/src/wkwebview/download.rs
+++ b/src/wkwebview/download.rs
@@ -1,123 +1,106 @@
-use std::{path::PathBuf, ptr::null_mut, rc::Rc};
+use std::{path::PathBuf, ptr::null_mut};
 
-use cocoa::base::id;
-use objc::{
-  declare::ClassDecl,
-  runtime::{Object, Sel},
-};
-use std::ffi::c_void;
-
-use super::NSString;
+use objc2::{rc::Retained, runtime::ProtocolObject, DeclaredClass};
+use objc2_foundation::{NSData, NSError, NSString, NSURLResponse, NSURL};
+use objc2_web_kit::{WKDownload, WKNavigationAction, WKNavigationResponse};
 
-pub(crate) unsafe fn set_download_delegate(webview: *mut Object, download_delegate: *mut Object) {
-  (*webview).set_ivar(
-    "DownloadDelegate",
-    download_delegate as *mut _ as *mut c_void,
-  );
-}
+#[cfg(target_os = "ios")]
+use crate::wkwebview::ios::WKWebView::WKWebView;
+#[cfg(target_os = "macos")]
+use objc2_web_kit::WKWebView;
 
-unsafe fn get_download_delegate(this: &mut Object) -> *mut objc::runtime::Object {
-  let delegate: *mut c_void = *this.get_ivar("DownloadDelegate");
-  delegate as *mut Object
-}
+use super::class::{
+  wry_download_delegate::WryDownloadDelegate, wry_navigation_delegate::WryNavigationDelegate,
+};
 
 // Download action handler
-extern "C" fn navigation_download_action(this: &mut Object, _: Sel, _: id, _: id, download: id) {
+pub(crate) fn navigation_download_action(
+  this: &WryNavigationDelegate,
+  _webview: &WKWebView,
+  _action: &WKNavigationAction,
+  download: &WKDownload,
+) {
   unsafe {
-    let delegate = get_download_delegate(this);
-    let _: () = msg_send![download, setDelegate: delegate];
+    if let Some(delegate) = &this.ivars().download_delegate {
+      let proto_delegate = ProtocolObject::from_ref(delegate.as_ref());
+      download.setDelegate(Some(proto_delegate));
+    }
   }
 }
 
 // Download response handler
-extern "C" fn navigation_download_response(this: &mut Object, _: Sel, _: id, _: id, download: id) {
+pub(crate) fn navigation_download_response(
+  this: &WryNavigationDelegate,
+  _webview: &WKWebView,
+  _response: &WKNavigationResponse,
+  download: &WKDownload,
+) {
   unsafe {
-    let delegate = get_download_delegate(this);
-    let _: () = msg_send![download, setDelegate: delegate];
+    if let Some(delegate) = &this.ivars().download_delegate {
+      let proto_delegate = ProtocolObject::from_ref(delegate.as_ref());
+      download.setDelegate(Some(proto_delegate));
+    }
   }
 }
 
-pub(crate) unsafe fn add_download_methods(decl: &mut ClassDecl) {
-  decl.add_ivar::<*mut c_void>("DownloadDelegate");
-
-  decl.add_method(
-    sel!(webView:navigationAction:didBecomeDownload:),
-    navigation_download_action as extern "C" fn(&mut Object, Sel, id, id, id),
-  );
-
-  decl.add_method(
-    sel!(webView:navigationResponse:didBecomeDownload:),
-    navigation_download_response as extern "C" fn(&mut Object, Sel, id, id, id),
-  );
-}
-
-pub extern "C" fn download_policy(
-  this: &Object,
-  _: Sel,
-  download: id,
-  _: id,
-  suggested_path: id,
-  handler: id,
+pub(crate) fn download_policy(
+  this: &WryDownloadDelegate,
+  download: &WKDownload,
+  _response: &NSURLResponse,
+  suggested_path: &NSString,
+  completion_handler: &block2::Block<dyn Fn(*const NSURL)>,
 ) {
   unsafe {
-    let request: id = msg_send![download, originalRequest];
-    let url: id = msg_send![request, URL];
-    let url: id = msg_send![url, absoluteString];
-    let url = NSString(url);
-    let path = NSString(suggested_path);
-    let mut path = PathBuf::from(path.to_str());
-    let handler = handler as *mut block::Block<(id,), c_void>;
+    let request = download.originalRequest().unwrap();
+    let url = request.URL().unwrap().absoluteString().unwrap();
+    let mut path = PathBuf::from(suggested_path.to_string());
 
-    let function = this.get_ivar::<*mut c_void>("started");
-    if !function.is_null() {
-      let function = &mut *(*function as *mut Box<dyn for<'s> FnMut(String, &mut PathBuf) -> bool>);
-      match (function)(url.to_str().to_string(), &mut path) {
+    let started_fn = &this.ivars().started;
+    if let Some(started_fn) = started_fn {
+      let mut started_fn = started_fn.borrow_mut();
+      match started_fn(url.to_string().to_string(), &mut path) {
         true => {
-          let nsurl: id = msg_send![class!(NSURL), fileURLWithPath: NSString::new(&path.display().to_string()) isDirectory: false];
-          (*handler).call((nsurl,))
+          let path = NSString::from_str(&path.display().to_string());
+          let ns_url = NSURL::fileURLWithPath_isDirectory(&path, false);
+          (*completion_handler).call((Retained::as_ptr(&ns_url),))
         }
-        false => (*handler).call((null_mut(),)),
+        false => (*completion_handler).call((null_mut(),)),
       };
     } else {
       #[cfg(feature = "tracing")]
       tracing::warn!("WebView instance is dropped! This navigation handler shouldn't be called.");
-      (*handler).call((null_mut(),));
+      (*completion_handler).call((null_mut(),));
     }
   }
 }
 
-pub extern "C" fn download_did_finish(this: &Object, _: Sel, download: id) {
+pub(crate) fn download_did_finish(this: &WryDownloadDelegate, download: &WKDownload) {
   unsafe {
-    let function = this.get_ivar::<*mut c_void>("completed");
-    let original_request: id = msg_send![download, originalRequest];
-    let url: id = msg_send![original_request, URL];
-    let url: id = msg_send![url, absoluteString];
-    let url = NSString(url).to_str().to_string();
-    if !function.is_null() {
-      let function = &mut *(*function as *mut Rc<dyn for<'s> Fn(String, Option<PathBuf>, bool)>);
-      function(url, None, true);
+    let original_request = download.originalRequest().unwrap();
+    let url = original_request.URL().unwrap().absoluteString().unwrap();
+    if let Some(completed_fn) = this.ivars().completed.clone() {
+      completed_fn(url.to_string(), None, true);
     }
   }
 }
 
-pub extern "C" fn download_did_fail(this: &Object, _: Sel, download: id, _error: id, _: id) {
+pub(crate) fn download_did_fail(
+  this: &WryDownloadDelegate,
+  download: &WKDownload,
+  error: &NSError,
+  _resume_data: &NSData,
+) {
   unsafe {
     #[cfg(debug_assertions)]
     {
-      let description: id = msg_send![_error, localizedDescription];
-      let description = NSString(description).to_str().to_string();
+      let description = error.localizedDescription().to_string();
       eprintln!("Download failed with error: {}", description);
     }
 
-    let original_request: id = msg_send![download, originalRequest];
-    let url: id = msg_send![original_request, URL];
-    let url: id = msg_send![url, absoluteString];
-    let url = NSString(url).to_str().to_string();
-
-    let function = this.get_ivar::<*mut c_void>("completed");
-    if !function.is_null() {
-      let function = &mut *(*function as *mut Rc<dyn for<'s> Fn(String, Option<PathBuf>, bool)>);
-      function(url, None, false);
+    let original_request = download.originalRequest().unwrap();
+    let url = original_request.URL().unwrap().absoluteString().unwrap();
+    if let Some(completed_fn) = this.ivars().completed.clone() {
+      completed_fn(url.to_string(), None, false);
     }
   }
 }
diff --git a/src/wkwebview/drag_drop.rs b/src/wkwebview/drag_drop.rs
index 948d83df7..18ddaa3b1 100644
--- a/src/wkwebview/drag_drop.rs
+++ b/src/wkwebview/drag_drop.rs
@@ -2,179 +2,103 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use std::{
-  ffi::{c_void, CStr},
-  path::PathBuf,
-};
+use std::{ffi::CStr, path::PathBuf};
 
-use cocoa::{
-  base::{id, BOOL, YES},
-  foundation::{NSPoint, NSRect},
-};
-use objc::{
-  declare::ClassDecl,
-  runtime::{class_getInstanceMethod, method_getImplementation, Object, Sel},
+use objc2::{
+  rc::Id,
+  runtime::{AnyObject, Bool, ProtocolObject},
+  DeclaredClass,
 };
-use once_cell::sync::Lazy;
+use objc2_app_kit::{NSDragOperation, NSDraggingInfo, NSFilenamesPboardType};
+use objc2_foundation::{NSArray, NSPoint, NSRect, NSString};
 
 use crate::DragDropEvent;
 
-pub(crate) type NSDragOperation = cocoa::foundation::NSUInteger;
-
-#[allow(non_upper_case_globals)]
-const NSDragOperationCopy: NSDragOperation = 1;
-
-const DRAG_DROP_HANDLER_IVAR: &str = "DragDropHandler";
-
-static OBJC_DRAGGING_ENTERED: Lazy<extern "C" fn(*const Object, Sel, id) -> NSDragOperation> =
-  Lazy::new(|| unsafe {
-    std::mem::transmute(method_getImplementation(class_getInstanceMethod(
-      class!(WKWebView),
-      sel!(draggingEntered:),
-    )))
-  });
-
-static OBJC_DRAGGING_EXITED: Lazy<extern "C" fn(*const Object, Sel, id)> = Lazy::new(|| unsafe {
-  std::mem::transmute(method_getImplementation(class_getInstanceMethod(
-    class!(WKWebView),
-    sel!(draggingExited:),
-  )))
-});
-
-static OBJC_PERFORM_DRAG_OPERATION: Lazy<extern "C" fn(*const Object, Sel, id) -> BOOL> =
-  Lazy::new(|| unsafe {
-    std::mem::transmute(method_getImplementation(class_getInstanceMethod(
-      class!(WKWebView),
-      sel!(performDragOperation:),
-    )))
-  });
-
-static OBJC_DRAGGING_UPDATED: Lazy<extern "C" fn(*const Object, Sel, id) -> NSDragOperation> =
-  Lazy::new(|| unsafe {
-    std::mem::transmute(method_getImplementation(class_getInstanceMethod(
-      class!(WKWebView),
-      sel!(draggingUpdated:),
-    )))
-  });
+use super::WryWebView;
 
-// Safety: objc runtime calls are unsafe
-pub(crate) unsafe fn set_drag_drop_handler(
-  webview: *mut Object,
-  handler: Box<dyn Fn(DragDropEvent) -> bool>,
-) -> *mut Box<dyn Fn(DragDropEvent) -> bool> {
-  let listener = Box::into_raw(Box::new(handler));
-  (*webview).set_ivar(DRAG_DROP_HANDLER_IVAR, listener as *mut _ as *mut c_void);
-  listener
-}
-
-#[allow(clippy::mut_from_ref)]
-unsafe fn get_handler(this: &Object) -> &mut Box<dyn Fn(DragDropEvent) -> bool> {
-  let delegate: *mut c_void = *this.get_ivar(DRAG_DROP_HANDLER_IVAR);
-  &mut *(delegate as *mut Box<dyn Fn(DragDropEvent) -> bool>)
-}
-
-unsafe fn collect_paths(drag_info: id) -> Vec<PathBuf> {
-  use cocoa::{
-    appkit::{NSFilenamesPboardType, NSPasteboard},
-    foundation::{NSFastEnumeration, NSString},
-  };
-
-  let pb: id = msg_send![drag_info, draggingPasteboard];
+pub(crate) unsafe fn collect_paths(drag_info: &ProtocolObject<dyn NSDraggingInfo>) -> Vec<PathBuf> {
+  let pb = drag_info.draggingPasteboard();
   let mut drag_drop_paths = Vec::new();
-  let types: id = msg_send![class!(NSArray), arrayWithObject: NSFilenamesPboardType];
-  if !NSPasteboard::availableTypeFromArray(pb, types).is_null() {
-    for path in NSPasteboard::propertyListForType(pb, NSFilenamesPboardType).iter() {
-      drag_drop_paths.push(PathBuf::from(
-        CStr::from_ptr(NSString::UTF8String(path))
-          .to_string_lossy()
-          .into_owned(),
-      ));
+  let types = NSArray::arrayWithObject(NSFilenamesPboardType);
+
+  if pb.availableTypeFromArray(&types).is_some() {
+    let paths = pb.propertyListForType(NSFilenamesPboardType).unwrap();
+    let paths: Id<NSArray<NSString>> = Id::<AnyObject>::cast(paths.clone());
+    for path in paths.to_vec() {
+      let path = CStr::from_ptr(path.UTF8String()).to_string_lossy();
+      drag_drop_paths.push(PathBuf::from(path.into_owned()));
     }
   }
   drag_drop_paths
 }
 
-extern "C" fn dragging_updated(this: &mut Object, sel: Sel, drag_info: id) -> NSDragOperation {
-  let dl: NSPoint = unsafe { msg_send![drag_info, draggingLocation] };
-  let frame: NSRect = unsafe { msg_send![this, frame] };
+pub(crate) fn dragging_entered(
+  this: &WryWebView,
+  drag_info: &ProtocolObject<dyn NSDraggingInfo>,
+) -> NSDragOperation {
+  let paths = unsafe { collect_paths(drag_info) };
+  let dl: NSPoint = unsafe { drag_info.draggingLocation() };
+  let frame: NSRect = this.frame();
   let position = (dl.x as i32, (frame.size.height - dl.y) as i32);
-  let listener = unsafe { get_handler(this) };
-  if !listener(DragDropEvent::Over { position }) {
-    let os_operation = OBJC_DRAGGING_UPDATED(this, sel, drag_info);
-    if os_operation == 0 {
-      // 0 will be returned for a drop on any arbitrary location on the webview.
-      // We'll override that with NSDragOperationCopy.
-      NSDragOperationCopy
-    } else {
-      // A different NSDragOperation is returned when a file is hovered over something like
-      // a <input type="file">, so we'll make sure to preserve that behaviour.
-      os_operation
-    }
+
+  let listener = &this.ivars().drag_drop_handler;
+  if !listener(DragDropEvent::Enter { paths, position }) {
+    // Reject the Wry file drop (invoke the OS default behaviour)
+    unsafe { objc2::msg_send![super(this), draggingEntered: drag_info] }
   } else {
-    NSDragOperationCopy
+    NSDragOperation::Copy
   }
 }
 
-extern "C" fn dragging_entered(this: &mut Object, sel: Sel, drag_info: id) -> NSDragOperation {
-  let listener = unsafe { get_handler(this) };
-  let paths = unsafe { collect_paths(drag_info) };
-
-  let dl: NSPoint = unsafe { msg_send![drag_info, draggingLocation] };
-  let frame: NSRect = unsafe { msg_send![this, frame] };
+pub(crate) fn dragging_updated(
+  this: &WryWebView,
+  drag_info: &ProtocolObject<dyn NSDraggingInfo>,
+) -> NSDragOperation {
+  let dl: NSPoint = unsafe { drag_info.draggingLocation() };
+  let frame: NSRect = this.frame();
   let position = (dl.x as i32, (frame.size.height - dl.y) as i32);
 
-  if !listener(DragDropEvent::Enter { paths, position }) {
-    // Reject the Wry file drop (invoke the OS default behaviour)
-    OBJC_DRAGGING_ENTERED(this, sel, drag_info)
+  let listener = &this.ivars().drag_drop_handler;
+  if !listener(DragDropEvent::Over { position }) {
+    unsafe {
+      let os_operation = objc2::msg_send![super(this), draggingUpdated: drag_info];
+      if os_operation == NSDragOperation::None {
+        // 0 will be returned for a drop on any arbitrary location on the webview.
+        // We'll override that with NSDragOperationCopy.
+        NSDragOperation::Copy
+      } else {
+        // A different NSDragOperation is returned when a file is hovered over something like
+        // a <input type="file">, so we'll make sure to preserve that behaviour.
+        os_operation
+      }
+    }
   } else {
-    NSDragOperationCopy
+    NSDragOperation::Copy
   }
 }
 
-extern "C" fn perform_drag_operation(this: &mut Object, sel: Sel, drag_info: id) -> BOOL {
-  let listener = unsafe { get_handler(this) };
+pub(crate) fn perform_drag_operation(
+  this: &WryWebView,
+  drag_info: &ProtocolObject<dyn NSDraggingInfo>,
+) -> Bool {
   let paths = unsafe { collect_paths(drag_info) };
-
-  let dl: NSPoint = unsafe { msg_send![drag_info, draggingLocation] };
-  let frame: NSRect = unsafe { msg_send![this, frame] };
+  let dl: NSPoint = unsafe { drag_info.draggingLocation() };
+  let frame: NSRect = this.frame();
   let position = (dl.x as i32, (frame.size.height - dl.y) as i32);
 
+  let listener = &this.ivars().drag_drop_handler;
   if !listener(DragDropEvent::Drop { paths, position }) {
     // Reject the Wry drop (invoke the OS default behaviour)
-    OBJC_PERFORM_DRAG_OPERATION(this, sel, drag_info)
+    unsafe { objc2::msg_send![super(this), performDragOperation: drag_info] }
   } else {
-    YES
+    Bool::YES
   }
 }
 
-extern "C" fn dragging_exited(this: &mut Object, sel: Sel, drag_info: id) {
-  let listener = unsafe { get_handler(this) };
+pub(crate) fn dragging_exited(this: &WryWebView, drag_info: &ProtocolObject<dyn NSDraggingInfo>) {
+  let listener = &this.ivars().drag_drop_handler;
   if !listener(DragDropEvent::Leave) {
     // Reject the Wry drop (invoke the OS default behaviour)
-    OBJC_DRAGGING_EXITED(this, sel, drag_info);
+    unsafe { objc2::msg_send![super(this), draggingExited: drag_info] }
   }
 }
-
-pub(crate) unsafe fn add_drag_drop_methods(decl: &mut ClassDecl) {
-  decl.add_ivar::<*mut c_void>(DRAG_DROP_HANDLER_IVAR);
-
-  decl.add_method(
-    sel!(draggingEntered:),
-    dragging_entered as extern "C" fn(&mut Object, Sel, id) -> NSDragOperation,
-  );
-
-  decl.add_method(
-    sel!(draggingUpdated:),
-    dragging_updated as extern "C" fn(&mut Object, Sel, id) -> NSDragOperation,
-  );
-
-  decl.add_method(
-    sel!(performDragOperation:),
-    perform_drag_operation as extern "C" fn(&mut Object, Sel, id) -> BOOL,
-  );
-
-  decl.add_method(
-    sel!(draggingExited:),
-    dragging_exited as extern "C" fn(&mut Object, Sel, id),
-  );
-}
diff --git a/src/wkwebview/ios/WKWebView.rs b/src/wkwebview/ios/WKWebView.rs
new file mode 100644
index 000000000..72462cda6
--- /dev/null
+++ b/src/wkwebview/ios/WKWebView.rs
@@ -0,0 +1,621 @@
+#![allow(warnings)]
+#![allow(clippy::all)]
+
+//! This file has been automatically generated by `objc2`'s `header-translator`.
+//! DO NOT EDIT
+use objc2::__framework_prelude::*;
+use objc2_foundation::*;
+use objc2_ui_kit::*;
+use objc2_web_kit::*;
+
+use crate::*;
+
+// NS_ENUM
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct WKMediaPlaybackState(pub NSInteger);
+impl WKMediaPlaybackState {
+  #[doc(alias = "WKMediaPlaybackStateNone")]
+  pub const None: Self = Self(0);
+  #[doc(alias = "WKMediaPlaybackStatePlaying")]
+  pub const Playing: Self = Self(1);
+  #[doc(alias = "WKMediaPlaybackStatePaused")]
+  pub const Paused: Self = Self(2);
+  #[doc(alias = "WKMediaPlaybackStateSuspended")]
+  pub const Suspended: Self = Self(3);
+}
+
+unsafe impl Encode for WKMediaPlaybackState {
+  const ENCODING: Encoding = NSInteger::ENCODING;
+}
+
+unsafe impl RefEncode for WKMediaPlaybackState {
+  const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
+}
+
+// NS_ENUM
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct WKMediaCaptureState(pub NSInteger);
+impl WKMediaCaptureState {
+  #[doc(alias = "WKMediaCaptureStateNone")]
+  pub const None: Self = Self(0);
+  #[doc(alias = "WKMediaCaptureStateActive")]
+  pub const Active: Self = Self(1);
+  #[doc(alias = "WKMediaCaptureStateMuted")]
+  pub const Muted: Self = Self(2);
+}
+
+unsafe impl Encode for WKMediaCaptureState {
+  const ENCODING: Encoding = NSInteger::ENCODING;
+}
+
+unsafe impl RefEncode for WKMediaCaptureState {
+  const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
+}
+
+// NS_ENUM
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
+pub struct WKFullscreenState(pub NSInteger);
+impl WKFullscreenState {
+  #[doc(alias = "WKFullscreenStateNotInFullscreen")]
+  pub const NotInFullscreen: Self = Self(0);
+  #[doc(alias = "WKFullscreenStateEnteringFullscreen")]
+  pub const EnteringFullscreen: Self = Self(1);
+  #[doc(alias = "WKFullscreenStateInFullscreen")]
+  pub const InFullscreen: Self = Self(2);
+  #[doc(alias = "WKFullscreenStateExitingFullscreen")]
+  pub const ExitingFullscreen: Self = Self(3);
+}
+
+unsafe impl Encode for WKFullscreenState {
+  const ENCODING: Encoding = NSInteger::ENCODING;
+}
+
+unsafe impl RefEncode for WKFullscreenState {
+  const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING);
+}
+
+extern_class!(
+  #[derive(Debug, PartialEq, Eq, Hash)]
+  pub struct WKWebView;
+
+  unsafe impl ClassType for WKWebView {
+    #[inherits(UIResponder, NSObject)]
+    type Super = UIView;
+    type Mutability = MainThreadOnly;
+  }
+);
+
+#[cfg(target_os = "macos")]
+unsafe impl NSAccessibility for WKWebView {}
+
+#[cfg(target_os = "macos")]
+unsafe impl NSAccessibilityElementProtocol for WKWebView {}
+
+#[cfg(target_os = "macos")]
+unsafe impl NSAnimatablePropertyContainer for WKWebView {}
+
+#[cfg(target_os = "macos")]
+unsafe impl NSAppearanceCustomization for WKWebView {}
+
+unsafe impl NSCoding for WKWebView {}
+
+#[cfg(target_os = "macos")]
+unsafe impl NSDraggingDestination for WKWebView {}
+
+unsafe impl NSObjectProtocol for WKWebView {}
+
+#[cfg(target_os = "macos")]
+unsafe impl NSUserInterfaceItemIdentification for WKWebView {}
+
+extern_methods!(
+  unsafe impl WKWebView {
+    // #[cfg(feature = "WKWebViewConfiguration")]
+    #[method_id(@__retain_semantics Other configuration)]
+    pub unsafe fn configuration(&self) -> Retained<WKWebViewConfiguration>;
+
+    // #[cfg(feature = "WKNavigationDelegate")]
+    #[method_id(@__retain_semantics Other navigationDelegate)]
+    pub unsafe fn navigationDelegate(
+      &self,
+    ) -> Option<Retained<ProtocolObject<dyn WKNavigationDelegate>>>;
+
+    // #[cfg(feature = "WKNavigationDelegate")]
+    #[method(setNavigationDelegate:)]
+    pub unsafe fn setNavigationDelegate(
+      &self,
+      navigation_delegate: Option<&ProtocolObject<dyn WKNavigationDelegate>>,
+    );
+
+    // #[cfg(feature = "WKUIDelegate")]
+    #[method_id(@__retain_semantics Other UIDelegate)]
+    pub unsafe fn UIDelegate(&self) -> Option<Retained<ProtocolObject<dyn WKUIDelegate>>>;
+
+    // #[cfg(feature = "WKUIDelegate")]
+    #[method(setUIDelegate:)]
+    pub unsafe fn setUIDelegate(&self, ui_delegate: Option<&ProtocolObject<dyn WKUIDelegate>>);
+
+    #[cfg(target_os = "macos")]
+    // #[cfg(feature = "WKBackForwardList")]
+    #[method_id(@__retain_semantics Other backForwardList)]
+    pub unsafe fn backForwardList(&self) -> Retained<WKBackForwardList>;
+
+    // #[cfg(feature = "WKWebViewConfiguration")]
+    #[method_id(@__retain_semantics Init initWithFrame:configuration:)]
+    pub unsafe fn initWithFrame_configuration(
+      this: Allocated<Self>,
+      frame: CGRect,
+      configuration: &WKWebViewConfiguration,
+    ) -> Retained<Self>;
+
+    #[method_id(@__retain_semantics Init initWithCoder:)]
+    pub unsafe fn initWithCoder(this: Allocated<Self>, coder: &NSCoder) -> Option<Retained<Self>>;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[method_id(@__retain_semantics Other loadRequest:)]
+    pub unsafe fn loadRequest(&self, request: &NSURLRequest) -> Option<Retained<WKNavigation>>;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[method_id(@__retain_semantics Other loadFileURL:allowingReadAccessToURL:)]
+    pub unsafe fn loadFileURL_allowingReadAccessToURL(
+      &self,
+      url: &NSURL,
+      read_access_url: &NSURL,
+    ) -> Option<Retained<WKNavigation>>;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[method_id(@__retain_semantics Other loadHTMLString:baseURL:)]
+    pub unsafe fn loadHTMLString_baseURL(
+      &self,
+      string: &NSString,
+      base_url: Option<&NSURL>,
+    ) -> Option<Retained<WKNavigation>>;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[method_id(@__retain_semantics Other loadData:MIMEType:characterEncodingName:baseURL:)]
+    pub unsafe fn loadData_MIMEType_characterEncodingName_baseURL(
+      &self,
+      data: &NSData,
+      mime_type: &NSString,
+      character_encoding_name: &NSString,
+      base_url: &NSURL,
+    ) -> Option<Retained<WKNavigation>>;
+
+    #[cfg(target_os = "macos")]
+    // #[cfg(all(feature = "WKBackForwardListItem", feature = "WKNavigation"))]
+    #[method_id(@__retain_semantics Other goToBackForwardListItem:)]
+    pub unsafe fn goToBackForwardListItem(
+      &self,
+      item: &WKBackForwardListItem,
+    ) -> Option<Retained<WKNavigation>>;
+
+    #[method_id(@__retain_semantics Other title)]
+    pub unsafe fn title(&self) -> Option<Retained<NSString>>;
+
+    #[method_id(@__retain_semantics Other URL)]
+    pub unsafe fn URL(&self) -> Option<Retained<NSURL>>;
+
+    #[method(isLoading)]
+    pub unsafe fn isLoading(&self) -> bool;
+
+    #[method(estimatedProgress)]
+    pub unsafe fn estimatedProgress(&self) -> c_double;
+
+    #[method(hasOnlySecureContent)]
+    pub unsafe fn hasOnlySecureContent(&self) -> bool;
+
+    #[method(canGoBack)]
+    pub unsafe fn canGoBack(&self) -> bool;
+
+    #[method(canGoForward)]
+    pub unsafe fn canGoForward(&self) -> bool;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[method_id(@__retain_semantics Other goBack)]
+    pub unsafe fn goBack(&self) -> Option<Retained<WKNavigation>>;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[method_id(@__retain_semantics Other goForward)]
+    pub unsafe fn goForward(&self) -> Option<Retained<WKNavigation>>;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[method_id(@__retain_semantics Other reload)]
+    pub unsafe fn reload(&self) -> Option<Retained<WKNavigation>>;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[method_id(@__retain_semantics Other reloadFromOrigin)]
+    pub unsafe fn reloadFromOrigin(&self) -> Option<Retained<WKNavigation>>;
+
+    #[method(stopLoading)]
+    pub unsafe fn stopLoading(&self);
+
+    // #[cfg(feature = "block2")]
+    #[method(evaluateJavaScript:completionHandler:)]
+    pub unsafe fn evaluateJavaScript_completionHandler(
+      &self,
+      java_script_string: &NSString,
+      completion_handler: Option<&block2::Block<dyn Fn(*mut AnyObject, *mut NSError)>>,
+    );
+
+    #[cfg(target_os = "macos")]
+    // #[cfg(all(
+    //   feature = "WKContentWorld",
+    //   feature = "WKFrameInfo",
+    //   feature = "block2"
+    // ))]
+    #[method(evaluateJavaScript:inFrame:inContentWorld:completionHandler:)]
+    pub unsafe fn evaluateJavaScript_inFrame_inContentWorld_completionHandler(
+      &self,
+      java_script_string: &NSString,
+      frame: Option<&WKFrameInfo>,
+      content_world: &WKContentWorld,
+      completion_handler: Option<&block2::Block<dyn Fn(*mut AnyObject, *mut NSError)>>,
+    );
+
+    #[cfg(target_os = "macos")]
+    // #[cfg(all(
+    //   feature = "WKContentWorld",
+    //   feature = "WKFrameInfo",
+    //   feature = "block2"
+    // ))]
+    #[method(callAsyncJavaScript:arguments:inFrame:inContentWorld:completionHandler:)]
+    pub unsafe fn callAsyncJavaScript_arguments_inFrame_inContentWorld_completionHandler(
+      &self,
+      function_body: &NSString,
+      arguments: Option<&NSDictionary<NSString, AnyObject>>,
+      frame: Option<&WKFrameInfo>,
+      content_world: &WKContentWorld,
+      completion_handler: Option<&block2::Block<dyn Fn(*mut AnyObject, *mut NSError)>>,
+    );
+
+    // #[cfg(feature = "block2")]
+    #[method(closeAllMediaPresentationsWithCompletionHandler:)]
+    pub unsafe fn closeAllMediaPresentationsWithCompletionHandler(
+      &self,
+      completion_handler: Option<&block2::Block<dyn Fn()>>,
+    );
+
+    #[deprecated]
+    #[method(closeAllMediaPresentations)]
+    pub unsafe fn closeAllMediaPresentations(&self);
+
+    // #[cfg(feature = "block2")]
+    #[method(pauseAllMediaPlaybackWithCompletionHandler:)]
+    pub unsafe fn pauseAllMediaPlaybackWithCompletionHandler(
+      &self,
+      completion_handler: Option<&block2::Block<dyn Fn()>>,
+    );
+
+    // #[cfg(feature = "block2")]
+    #[deprecated]
+    #[method(pauseAllMediaPlayback:)]
+    pub unsafe fn pauseAllMediaPlayback(
+      &self,
+      completion_handler: Option<&block2::Block<dyn Fn()>>,
+    );
+
+    // #[cfg(feature = "block2")]
+    #[method(setAllMediaPlaybackSuspended:completionHandler:)]
+    pub unsafe fn setAllMediaPlaybackSuspended_completionHandler(
+      &self,
+      suspended: bool,
+      completion_handler: Option<&block2::Block<dyn Fn()>>,
+    );
+
+    // #[cfg(feature = "block2")]
+    #[deprecated]
+    #[method(resumeAllMediaPlayback:)]
+    pub unsafe fn resumeAllMediaPlayback(
+      &self,
+      completion_handler: Option<&block2::Block<dyn Fn()>>,
+    );
+
+    // #[cfg(feature = "block2")]
+    #[deprecated]
+    #[method(suspendAllMediaPlayback:)]
+    pub unsafe fn suspendAllMediaPlayback(
+      &self,
+      completion_handler: Option<&block2::Block<dyn Fn()>>,
+    );
+
+    // #[cfg(feature = "block2")]
+    #[method(requestMediaPlaybackStateWithCompletionHandler:)]
+    pub unsafe fn requestMediaPlaybackStateWithCompletionHandler(
+      &self,
+      completion_handler: &block2::Block<dyn Fn(WKMediaPlaybackState)>,
+    );
+
+    // #[cfg(feature = "block2")]
+    #[deprecated]
+    #[method(requestMediaPlaybackState:)]
+    pub unsafe fn requestMediaPlaybackState(
+      &self,
+      completion_handler: &block2::Block<dyn Fn(WKMediaPlaybackState)>,
+    );
+
+    #[method(cameraCaptureState)]
+    pub unsafe fn cameraCaptureState(&self) -> WKMediaCaptureState;
+
+    #[method(microphoneCaptureState)]
+    pub unsafe fn microphoneCaptureState(&self) -> WKMediaCaptureState;
+
+    // #[cfg(feature = "block2")]
+    #[method(setCameraCaptureState:completionHandler:)]
+    pub unsafe fn setCameraCaptureState_completionHandler(
+      &self,
+      state: WKMediaCaptureState,
+      completion_handler: Option<&block2::Block<dyn Fn()>>,
+    );
+
+    // #[cfg(feature = "block2")]
+    #[method(setMicrophoneCaptureState:completionHandler:)]
+    pub unsafe fn setMicrophoneCaptureState_completionHandler(
+      &self,
+      state: WKMediaCaptureState,
+      completion_handler: Option<&block2::Block<dyn Fn()>>,
+    );
+
+    #[cfg(target_os = "macos")]
+    // #[cfg(all(feature = "WKSnapshotConfiguration", feature = "block2"))]
+    #[method(takeSnapshotWithConfiguration:completionHandler:)]
+    pub unsafe fn takeSnapshotWithConfiguration_completionHandler(
+      &self,
+      snapshot_configuration: Option<&WKSnapshotConfiguration>,
+      completion_handler: &block2::Block<dyn Fn(*mut NSImage, *mut NSError)>,
+    );
+
+    #[cfg(target_os = "macos")]
+    // #[cfg(all(feature = "WKPDFConfiguration", feature = "block2"))]
+    #[method(createPDFWithConfiguration:completionHandler:)]
+    pub unsafe fn createPDFWithConfiguration_completionHandler(
+      &self,
+      pdf_configuration: Option<&WKPDFConfiguration>,
+      completion_handler: &block2::Block<dyn Fn(*mut NSData, *mut NSError)>,
+    );
+
+    // #[cfg(feature = "block2")]
+    #[method(createWebArchiveDataWithCompletionHandler:)]
+    pub unsafe fn createWebArchiveDataWithCompletionHandler(
+      &self,
+      completion_handler: &block2::Block<dyn Fn(NonNull<NSData>, NonNull<NSError>)>,
+    );
+
+    #[method(allowsBackForwardNavigationGestures)]
+    pub unsafe fn allowsBackForwardNavigationGestures(&self) -> bool;
+
+    #[method(setAllowsBackForwardNavigationGestures:)]
+    pub unsafe fn setAllowsBackForwardNavigationGestures(
+      &self,
+      allows_back_forward_navigation_gestures: bool,
+    );
+
+    #[method_id(@__retain_semantics Other customUserAgent)]
+    pub unsafe fn customUserAgent(&self) -> Option<Retained<NSString>>;
+
+    #[method(setCustomUserAgent:)]
+    pub unsafe fn setCustomUserAgent(&self, custom_user_agent: Option<&NSString>);
+
+    #[method(allowsLinkPreview)]
+    pub unsafe fn allowsLinkPreview(&self) -> bool;
+
+    #[method(setAllowsLinkPreview:)]
+    pub unsafe fn setAllowsLinkPreview(&self, allows_link_preview: bool);
+
+    #[method(allowsMagnification)]
+    pub unsafe fn allowsMagnification(&self) -> bool;
+
+    #[method(setAllowsMagnification:)]
+    pub unsafe fn setAllowsMagnification(&self, allows_magnification: bool);
+
+    #[method(magnification)]
+    pub unsafe fn magnification(&self) -> CGFloat;
+
+    #[method(setMagnification:)]
+    pub unsafe fn setMagnification(&self, magnification: CGFloat);
+
+    #[method(setMagnification:centeredAtPoint:)]
+    pub unsafe fn setMagnification_centeredAtPoint(&self, magnification: CGFloat, point: CGPoint);
+
+    #[method(pageZoom)]
+    pub unsafe fn pageZoom(&self) -> CGFloat;
+
+    #[method(setPageZoom:)]
+    pub unsafe fn setPageZoom(&self, page_zoom: CGFloat);
+
+    #[cfg(target_os = "macos")]
+    // #[cfg(all(
+    //   feature = "WKFindConfiguration",
+    //   feature = "WKFindResult",
+    //   feature = "block2"
+    // ))]
+    #[method(findString:withConfiguration:completionHandler:)]
+    pub unsafe fn findString_withConfiguration_completionHandler(
+      &self,
+      string: &NSString,
+      configuration: Option<&WKFindConfiguration>,
+      completion_handler: &block2::Block<dyn Fn(NonNull<WKFindResult>)>,
+    );
+
+    #[method(handlesURLScheme:)]
+    pub unsafe fn handlesURLScheme(url_scheme: &NSString, mtm: MainThreadMarker) -> bool;
+
+    // #[cfg(all(feature = "WKDownload", feature = "block2"))]
+    #[method(startDownloadUsingRequest:completionHandler:)]
+    pub unsafe fn startDownloadUsingRequest_completionHandler(
+      &self,
+      request: &NSURLRequest,
+      completion_handler: &block2::Block<dyn Fn(NonNull<WKDownload>)>,
+    );
+
+    // #[cfg(all(feature = "WKDownload", feature = "block2"))]
+    #[method(resumeDownloadFromResumeData:completionHandler:)]
+    pub unsafe fn resumeDownloadFromResumeData_completionHandler(
+      &self,
+      resume_data: &NSData,
+      completion_handler: &block2::Block<dyn Fn(NonNull<WKDownload>)>,
+    );
+
+    #[method_id(@__retain_semantics Other mediaType)]
+    pub unsafe fn mediaType(&self) -> Option<Retained<NSString>>;
+
+    #[method(setMediaType:)]
+    pub unsafe fn setMediaType(&self, media_type: Option<&NSString>);
+
+    #[method_id(@__retain_semantics Other interactionState)]
+    pub unsafe fn interactionState(&self) -> Option<Retained<AnyObject>>;
+
+    #[method(setInteractionState:)]
+    pub unsafe fn setInteractionState(&self, interaction_state: Option<&AnyObject>);
+
+    // #[cfg(feature = "WKNavigation")]
+    #[method_id(@__retain_semantics Other loadSimulatedRequest:response:responseData:)]
+    pub unsafe fn loadSimulatedRequest_response_responseData(
+      &self,
+      request: &NSURLRequest,
+      response: &NSURLResponse,
+      data: &NSData,
+    ) -> Retained<WKNavigation>;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[deprecated]
+    #[method_id(@__retain_semantics Other loadSimulatedRequest:withResponse:responseData:)]
+    pub unsafe fn loadSimulatedRequest_withResponse_responseData(
+      &self,
+      request: &NSURLRequest,
+      response: &NSURLResponse,
+      data: &NSData,
+    ) -> Retained<WKNavigation>;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[method_id(@__retain_semantics Other loadFileRequest:allowingReadAccessToURL:)]
+    pub unsafe fn loadFileRequest_allowingReadAccessToURL(
+      &self,
+      request: &NSURLRequest,
+      read_access_url: &NSURL,
+    ) -> Retained<WKNavigation>;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[method_id(@__retain_semantics Other loadSimulatedRequest:responseHTMLString:)]
+    pub unsafe fn loadSimulatedRequest_responseHTMLString(
+      &self,
+      request: &NSURLRequest,
+      string: &NSString,
+    ) -> Retained<WKNavigation>;
+
+    // #[cfg(feature = "WKNavigation")]
+    #[deprecated]
+    #[method_id(@__retain_semantics Other loadSimulatedRequest:withResponseHTMLString:)]
+    pub unsafe fn loadSimulatedRequest_withResponseHTMLString(
+      &self,
+      request: &NSURLRequest,
+      string: &NSString,
+    ) -> Retained<WKNavigation>;
+
+    #[cfg(target_os = "macos")]
+    #[method_id(@__retain_semantics Other printOperationWithPrintInfo:)]
+    pub unsafe fn printOperationWithPrintInfo(
+      &self,
+      print_info: &NSPrintInfo,
+    ) -> Retained<NSPrintOperation>;
+
+    #[cfg(target_os = "macos")]
+    #[method_id(@__retain_semantics Other themeColor)]
+    pub unsafe fn themeColor(&self) -> Option<Retained<NSColor>>;
+
+    #[cfg(target_os = "macos")]
+    #[method_id(@__retain_semantics Other underPageBackgroundColor)]
+    pub unsafe fn underPageBackgroundColor(&self) -> Retained<NSColor>;
+
+    #[cfg(target_os = "macos")]
+    #[method(setUnderPageBackgroundColor:)]
+    pub unsafe fn setUnderPageBackgroundColor(&self, under_page_background_color: Option<&NSColor>);
+
+    #[method(fullscreenState)]
+    pub unsafe fn fullscreenState(&self) -> WKFullscreenState;
+
+    #[method(minimumViewportInset)]
+    pub unsafe fn minimumViewportInset(&self) -> NSEdgeInsets;
+
+    #[method(maximumViewportInset)]
+    pub unsafe fn maximumViewportInset(&self) -> NSEdgeInsets;
+
+    #[method(setMinimumViewportInset:maximumViewportInset:)]
+    pub unsafe fn setMinimumViewportInset_maximumViewportInset(
+      &self,
+      minimum_viewport_inset: NSEdgeInsets,
+      maximum_viewport_inset: NSEdgeInsets,
+    );
+
+    #[method(isInspectable)]
+    pub unsafe fn isInspectable(&self) -> bool;
+
+    #[method(setInspectable:)]
+    pub unsafe fn setInspectable(&self, inspectable: bool);
+  }
+);
+
+extern_methods!(
+  /// Methods declared on superclass `UIView`
+  unsafe impl WKWebView {
+    #[method_id(@__retain_semantics Init initWithFrame:)]
+    pub unsafe fn initWithFrame(this: Allocated<Self>, frame_rect: NSRect) -> Retained<Self>;
+  }
+);
+
+extern_methods!(
+  /// Methods declared on superclass `UIResponder`
+  unsafe impl WKWebView {
+    #[method_id(@__retain_semantics Init init)]
+    pub unsafe fn init(this: Allocated<Self>) -> Retained<Self>;
+  }
+);
+
+extern_methods!(
+  /// Methods declared on superclass `NSObject`
+  unsafe impl WKWebView {
+    #[method_id(@__retain_semantics New new)]
+    pub unsafe fn new(mtm: MainThreadMarker) -> Retained<Self>;
+  }
+);
+
+extern_methods!(
+  /// WKIBActions
+  unsafe impl WKWebView {
+    #[method(goBack:)]
+    pub unsafe fn goBack_(&self, sender: Option<&AnyObject>);
+
+    #[method(goForward:)]
+    pub unsafe fn goForward_(&self, sender: Option<&AnyObject>);
+
+    #[method(reload:)]
+    pub unsafe fn reload_(&self, sender: Option<&AnyObject>);
+
+    #[method(reloadFromOrigin:)]
+    pub unsafe fn reloadFromOrigin_(&self, sender: Option<&AnyObject>);
+
+    #[method(stopLoading:)]
+    pub unsafe fn stopLoading_(&self, sender: Option<&AnyObject>);
+  }
+);
+
+#[cfg(target_os = "macos")]
+unsafe impl NSUserInterfaceValidations for WKWebView {}
+
+extern_methods!(
+  /// WKNSTextFinderClient
+  unsafe impl WKWebView {}
+);
+
+#[cfg(target_os = "macos")]
+unsafe impl NSTextFinderClient for WKWebView {}
+
+extern_methods!(
+  /// WKDeprecated
+  unsafe impl WKWebView {
+    #[deprecated]
+    #[method_id(@__retain_semantics Other certificateChain)]
+    pub unsafe fn certificateChain(&self) -> Retained<NSArray>;
+  }
+);
diff --git a/src/wkwebview/ios/mod.rs b/src/wkwebview/ios/mod.rs
new file mode 100644
index 000000000..64ecbe707
--- /dev/null
+++ b/src/wkwebview/ios/mod.rs
@@ -0,0 +1,1 @@
+pub mod WKWebView;
diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs
index 1aadc0a5b..7669a76cb 100644
--- a/src/wkwebview/mod.rs
+++ b/src/wkwebview/mod.rs
@@ -1,4 +1,4 @@
-// Copyright 2020-2023 Tauri Programme within The Commons Conservancy
+// Copyright 2020-2024 Tauri Programme within The Commons Conservancy
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
@@ -12,41 +12,72 @@ mod proxy;
 mod synthetic_mouse_events;
 mod util;
 
+#[cfg(target_os = "ios")]
+mod ios;
+
+mod class;
+pub use class::wry_web_view::WryWebView;
 #[cfg(target_os = "macos")]
-use cocoa::appkit::{NSView, NSViewHeightSizable, NSViewMinYMargin, NSViewWidthSizable};
-use cocoa::{
-  base::{id, nil, NO, YES},
-  foundation::{NSDictionary, NSFastEnumeration, NSInteger},
+use class::wry_web_view_parent::WryWebViewParent;
+use class::{
+  document_title_changed_observer::*,
+  url_scheme_handler,
+  wry_download_delegate::WryDownloadDelegate,
+  wry_navigation_delegate::WryNavigationDelegate,
+  wry_web_view::WryWebViewIvars,
+  wry_web_view_delegate::{WryWebViewDelegate, IPC_MESSAGE_HANDLER_NAME},
+  wry_web_view_ui_delegate::WryWebViewUIDelegate,
 };
+
 use dpi::{LogicalPosition, LogicalSize};
+#[cfg(target_os = "macos")]
+use objc2::runtime::Bool;
+use objc2::{
+  rc::Retained,
+  runtime::{AnyObject, NSObject, ProtocolObject},
+  ClassType, DeclaredClass,
+};
+#[cfg(target_os = "macos")]
+use objc2_app_kit::{NSApplication, NSAutoresizingMaskOptions, NSTitlebarSeparatorStyle, NSView};
+#[cfg(target_os = "macos")]
+use objc2_foundation::CGSize;
+use objc2_foundation::{
+  ns_string, CGPoint, CGRect, MainThreadMarker, NSBundle, NSDate, NSError, NSJSONSerialization,
+  NSMutableURLRequest, NSNumber, NSObjectNSKeyValueCoding, NSObjectProtocol, NSString,
+  NSUTF8StringEncoding, NSURL, NSUUID,
+};
+#[cfg(target_os = "ios")]
+use objc2_ui_kit::{UIScrollView, UIViewAutoresizing};
+
+#[cfg(target_os = "macos")]
+use objc2_app_kit::NSWindow;
+#[cfg(target_os = "ios")]
+use objc2_ui_kit::UIView as NSView;
+// #[cfg(target_os = "ios")]
+// use objc2_ui_kit::UIWindow as NSWindow;
+
+#[cfg(target_os = "ios")]
+use crate::wkwebview::ios::WKWebView::WKWebView;
+#[cfg(target_os = "macos")]
+use objc2_web_kit::WKWebView;
+
+use objc2_web_kit::{
+  WKAudiovisualMediaTypes, WKURLSchemeHandler, WKUserContentController, WKUserScript,
+  WKUserScriptInjectionTime, WKWebViewConfiguration, WKWebsiteDataStore,
+};
 use once_cell::sync::Lazy;
 use raw_window_handle::{HasWindowHandle, RawWindowHandle};
 
 use std::{
-  borrow::Cow,
-  collections::HashSet,
-  ffi::{c_void, CStr, CString},
+  collections::{HashMap, HashSet},
+  ffi::{c_void, CString},
   os::raw::c_char,
-  ptr::{null, null_mut},
-  slice, str,
+  panic::AssertUnwindSafe,
+  ptr::null_mut,
+  str,
   sync::{Arc, Mutex},
 };
 
-use core_graphics::{
-  base::CGFloat,
-  geometry::{CGPoint, CGRect, CGSize},
-};
-
-use objc::{
-  declare::ClassDecl,
-  runtime::{Class, Object, Sel, BOOL},
-  Message,
-};
-use objc_id::Id;
-
-#[cfg(target_os = "macos")]
-use drag_drop::{add_drag_drop_methods, set_drag_drop_handler};
-
 #[cfg(feature = "mac-proxy")]
 use crate::{
   proxy::ProxyConfig,
@@ -55,32 +86,12 @@ use crate::{
   },
 };
 
-use crate::{
-  wkwebview::{
-    download::{
-      add_download_methods, download_did_fail, download_did_finish, download_policy,
-      set_download_delegate,
-    },
-    navigation::{add_navigation_mathods, drop_navigation_methods, set_navigation_methods},
-  },
-  Error, PageLoadEvent, Rect, RequestAsyncResponder, Result, WebViewAttributes, RGBA,
-};
+use crate::{Error, Rect, RequestAsyncResponder, Result, WebViewAttributes, RGBA};
 
-use http::{
-  header::{CONTENT_LENGTH, CONTENT_TYPE},
-  status::StatusCode,
-  version::Version,
-  Request, Response as HttpResponse,
-};
+use http::Request;
 
 use crate::util::Counter;
 
-const IPC_MESSAGE_HANDLER_NAME: &str = "ipc";
-#[cfg(target_os = "macos")]
-const ACCEPT_FIRST_MOUSE: &str = "accept_first_mouse";
-
-const NS_JSON_WRITING_FRAGMENTS_ALLOWED: u64 = 4;
-
 static COUNTER: Counter = Counter::new();
 static WEBVIEW_IDS: Lazy<Mutex<HashSet<String>>> = Lazy::new(Default::default);
 
@@ -99,19 +110,26 @@ pub struct PrintOptions {
 
 pub(crate) struct InnerWebView {
   id: String,
-  pub webview: id,
-  pub manager: id,
+  pub webview: Retained<WryWebView>,
+  pub manager: Retained<WKUserContentController>,
+  #[allow(dead_code)]
   is_child: bool,
   pending_scripts: Arc<Mutex<Option<Vec<String>>>>,
   // Note that if following functions signatures are changed in the future,
   // all functions pointer declarations in objc callbacks below all need to get updated.
-  ipc_handler_ptr: *mut Box<dyn Fn(Request<String>)>,
-  document_title_changed_handler: *mut Box<dyn Fn(String)>,
-  navigation_decide_policy_ptr: *mut Box<dyn Fn(String, bool) -> bool>,
-  page_load_handler: *mut Box<dyn Fn(PageLoadEvent)>,
-  #[cfg(target_os = "macos")]
-  drag_drop_ptr: *mut Box<dyn Fn(crate::DragDropEvent) -> bool>,
-  download_delegate: id,
+  ipc_handler_delegate: Option<Retained<WryWebViewDelegate>>,
+  #[allow(dead_code)]
+  // We need this the keep the reference count
+  document_title_changed_observer: Option<Retained<DocumentTitleChangedObserver>>,
+  #[allow(dead_code)]
+  // We need this the keep the reference count
+  navigation_policy_delegate: Retained<WryNavigationDelegate>,
+  #[allow(dead_code)]
+  // We need this the keep the reference count
+  download_delegate: Option<Retained<WryDownloadDelegate>>,
+  #[allow(dead_code)]
+  // We need this the keep the reference count
+  ui_delegate: Retained<WryWebViewUIDelegate>,
   protocol_ptrs: Vec<*mut Box<dyn Fn(crate::WebViewId, Request<Vec<u8>>, RequestAsyncResponder)>>,
 }
 
@@ -129,7 +147,7 @@ impl InnerWebView {
       _ => return Err(Error::UnsupportedWindowHandle),
     };
 
-    Self::new_ns_view(ns_view as _, attributes, pl_attrs, false)
+    unsafe { Self::new_ns_view(&*(ns_view as *mut NSView), attributes, pl_attrs, false) }
   }
 
   pub fn new_as_child(
@@ -145,229 +163,16 @@ impl InnerWebView {
       _ => return Err(Error::UnsupportedWindowHandle),
     };
 
-    Self::new_ns_view(ns_view as _, attributes, pl_attrs, true)
+    unsafe { Self::new_ns_view(&*(ns_view as *mut NSView), attributes, pl_attrs, true) }
   }
 
   fn new_ns_view(
-    ns_view: id,
+    ns_view: &NSView,
     attributes: WebViewAttributes,
     pl_attrs: super::PlatformSpecificWebViewAttributes,
     is_child: bool,
   ) -> Result<Self> {
-    // Function for ipc handler
-    extern "C" fn did_receive(this: &Object, _: Sel, _: id, msg: id) {
-      // Safety: objc runtime calls are unsafe
-      unsafe {
-        #[cfg(feature = "tracing")]
-        let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered();
-
-        let function = this.get_ivar::<*mut c_void>("function");
-        if !function.is_null() {
-          let function = &mut *(*function as *mut Box<dyn Fn(Request<String>)>);
-          let body: id = msg_send![msg, body];
-          let is_string: bool = msg_send![body, isKindOfClass: class!(NSString)];
-          if is_string {
-            let js_utf8: *const c_char = msg_send![body, UTF8String];
-
-            let frame_info: id = msg_send![msg, frameInfo];
-            let request: id = msg_send![frame_info, request];
-            let url: id = msg_send![request, URL];
-            let absolute_url: id = msg_send![url, absoluteString];
-            let url_utf8: *const c_char = msg_send![absolute_url, UTF8String];
-
-            if let (Ok(url), Ok(js)) = (
-              CStr::from_ptr(url_utf8).to_str(),
-              CStr::from_ptr(js_utf8).to_str(),
-            ) {
-              (function)(Request::builder().uri(url).body(js.to_string()).unwrap());
-              return;
-            }
-          }
-        }
-
-        #[cfg(feature = "tracing")]
-        tracing::warn!("WebView received invalid IPC call.");
-      }
-    }
-
-    // Task handler for custom protocol
-    extern "C" fn start_task(this: &Object, _: Sel, _webview: id, task: *mut Object) {
-      unsafe {
-        #[cfg(feature = "tracing")]
-        let span = tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty)
-          .entered();
-
-        let webview_id_ptr: *mut c_char = *this.get_ivar("id");
-        let webview_id = CStr::from_ptr(webview_id_ptr)
-          .to_str()
-          .ok()
-          .unwrap_or_default();
-
-        let function = this.get_ivar::<*mut c_void>("function");
-        if !function.is_null() {
-          let function = &mut *(*function
-            as *mut Box<dyn Fn(crate::WebViewId, Request<Vec<u8>>, RequestAsyncResponder)>);
-
-          // Get url request
-          let request: id = msg_send![task, request];
-          let url: id = msg_send![request, URL];
-
-          let uri_nsstring = {
-            let s: id = msg_send![url, absoluteString];
-            NSString(s)
-          };
-          let uri = uri_nsstring.to_str();
-
-          #[cfg(feature = "tracing")]
-          span.record("uri", uri);
-
-          // Get request method (GET, POST, PUT etc...)
-          let method = {
-            let s: id = msg_send![request, HTTPMethod];
-            NSString(s)
-          };
-
-          // Prepare our HttpRequest
-          let mut http_request = Request::builder().uri(uri).method(method.to_str());
-
-          // Get body
-          let mut sent_form_body = Vec::new();
-          let body: id = msg_send![request, HTTPBody];
-          let body_stream: id = msg_send![request, HTTPBodyStream];
-          if !body.is_null() {
-            let length = msg_send![body, length];
-            let data_bytes: id = msg_send![body, bytes];
-            if !data_bytes.is_null() {
-              sent_form_body = slice::from_raw_parts(data_bytes as *const u8, length).to_vec();
-            }
-          } else if !body_stream.is_null() {
-            let _: () = msg_send![body_stream, open];
-
-            while msg_send![body_stream, hasBytesAvailable] {
-              sent_form_body.reserve(128);
-              let p = sent_form_body.as_mut_ptr().add(sent_form_body.len());
-              let read_length = sent_form_body.capacity() - sent_form_body.len();
-              let count: usize = msg_send![body_stream, read: p maxLength: read_length];
-              sent_form_body.set_len(sent_form_body.len() + count);
-            }
-
-            let _: () = msg_send![body_stream, close];
-          }
-
-          // Extract all headers fields
-          let all_headers: id = msg_send![request, allHTTPHeaderFields];
-
-          // get all our headers values and inject them in our request
-          for current_header_ptr in all_headers.iter() {
-            let header_field = NSString(current_header_ptr);
-            let header_value = NSString(all_headers.valueForKey_(current_header_ptr));
-
-            // inject the header into the request
-            http_request = http_request.header(header_field.to_str(), header_value.to_str());
-          }
-
-          let respond_with_404 = || {
-            let urlresponse: id = msg_send![class!(NSHTTPURLResponse), alloc];
-            let response: id = msg_send![urlresponse, initWithURL:url statusCode:StatusCode::NOT_FOUND HTTPVersion:NSString::new(format!("{:#?}", Version::HTTP_11).as_str()) headerFields:null::<c_void>()];
-            let () = msg_send![task, didReceiveResponse: response];
-            // Finish
-            let () = msg_send![task, didFinish];
-          };
-
-          // send response
-          match http_request.body(sent_form_body) {
-            Ok(final_request) => {
-              let () = msg_send![task, retain];
-
-              let responder: Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)> = Box::new(
-                move |sent_response| {
-                  fn check_webview_id_valid(webview_id: &str) -> crate::Result<()> {
-                    match WEBVIEW_IDS.lock().unwrap().contains(webview_id) {
-                      true => Ok(()),
-                      false => Err(crate::Error::CustomProtocolTaskInvalid),
-                    }
-                  }
-
-                  unsafe fn response(
-                    task: id,
-                    webview_id: &str,
-                    url: id, /* NSURL */
-                    sent_response: HttpResponse<Cow<'_, [u8]>>,
-                  ) -> crate::Result<()> {
-                    let content = sent_response.body();
-                    // default: application/octet-stream, but should be provided by the client
-                    let wanted_mime = sent_response.headers().get(CONTENT_TYPE);
-                    // default to 200
-                    let wanted_status_code = sent_response.status().as_u16() as i32;
-                    // default to HTTP/1.1
-                    let wanted_version = format!("{:#?}", sent_response.version());
-
-                    let dictionary: id = msg_send![class!(NSMutableDictionary), alloc];
-                    let headers: id = msg_send![dictionary, initWithCapacity:1];
-                    if let Some(mime) = wanted_mime {
-                      let () = msg_send![headers, setObject:NSString::new(mime.to_str().unwrap()) forKey: NSString::new(CONTENT_TYPE.as_str())];
-                    }
-                    let () = msg_send![headers, setObject:NSString::new(&content.len().to_string()) forKey: NSString::new(CONTENT_LENGTH.as_str())];
-
-                    // add headers
-                    for (name, value) in sent_response.headers().iter() {
-                      let header_key = name.as_str();
-                      if let Ok(value) = value.to_str() {
-                        let () = msg_send![headers, setObject:NSString::new(value) forKey: NSString::new(header_key)];
-                      }
-                    }
-
-                    let urlresponse: id = msg_send![class!(NSHTTPURLResponse), alloc];
-                    let response: id = msg_send![urlresponse, initWithURL:url statusCode: wanted_status_code HTTPVersion:NSString::new(&wanted_version) headerFields:headers];
-
-                    check_webview_id_valid(&webview_id)?;
-                    (*task)
-                      .send_message::<(id,), ()>(sel!(didReceiveResponse:), (response,))
-                      .map_err(|_| crate::Error::CustomProtocolTaskInvalid)?;
-
-                    // Send data
-                    let bytes = content.as_ptr() as *mut c_void;
-                    let data: id = msg_send![class!(NSData), alloc];
-                    let data: id = msg_send![data, initWithBytesNoCopy:bytes length:content.len() freeWhenDone: if content.len() == 0 { NO } else { YES }];
-
-                    check_webview_id_valid(&webview_id)?;
-                    (*task)
-                      .send_message::<(id,), ()>(sel!(didReceiveData:), (data,))
-                      .map_err(|_| crate::Error::CustomProtocolTaskInvalid)?;
-
-                    // Finish
-                    check_webview_id_valid(&webview_id)?;
-                    (*task)
-                      .send_message::<(), ()>(sel!(didFinish), ())
-                      .map_err(|_| crate::Error::CustomProtocolTaskInvalid)?;
-
-                    Ok(())
-                  }
-
-                  let _ = response(task, &webview_id, url, sent_response);
-                  let () = msg_send![task, release];
-                },
-              );
-
-              #[cfg(feature = "tracing")]
-              let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered();
-              function(
-                webview_id,
-                final_request,
-                RequestAsyncResponder { responder },
-              );
-            }
-            Err(_) => respond_with_404(),
-          };
-        } else {
-          #[cfg(feature = "tracing")]
-          tracing::warn!(
-            "Either WebView or WebContext instance is dropped! This handler shouldn't be called."
-          );
-        }
-      }
-    }
-    extern "C" fn stop_task(_: &Object, _: Sel, _webview: id, _task: id) {}
+    let mtm = MainThreadMarker::new().ok_or(Error::NotMainThread)?;
 
     let webview_id = attributes
       .id
@@ -380,127 +185,81 @@ impl InnerWebView {
 
     // Safety: objc runtime calls are unsafe
     unsafe {
-      // Config and custom protocol
-      let config: id = msg_send![class!(WKWebViewConfiguration), new];
-      let mut protocol_ptrs = Vec::new();
+      let config = WKWebViewConfiguration::new();
 
+      // Incognito mode
       let os_version = util::operating_system_version();
-
       #[cfg(target_os = "macos")]
       let custom_data_store_available = os_version.0 >= 14;
-
       #[cfg(target_os = "ios")]
       let custom_data_store_available = os_version.0 >= 17;
 
-      let data_store: id = match (
+      let data_store = match (
         attributes.incognito,
         custom_data_store_available,
         pl_attrs.data_store_identifier,
       ) {
-        // incognito has priority
-        (true, _, _) => msg_send![class!(WKWebsiteDataStore), nonPersistentDataStore],
+        (true, _, _) => WKWebsiteDataStore::nonPersistentDataStore(),
         // if data_store_identifier is given and custom data stores are available, use custom store
         (false, true, Some(data_store)) => {
-          let ns_uuid = NSUUID::new(&data_store);
-          msg_send![class!(WKWebsiteDataStore), dataStoreForIdentifier:ns_uuid.0]
+          let identifier = NSUUID::from_bytes(data_store);
+          WKWebsiteDataStore::dataStoreForIdentifier(&identifier)
         }
         // default data store
-        _ => msg_send![class!(WKWebsiteDataStore), defaultDataStore],
+        _ => WKWebsiteDataStore::defaultDataStore(),
       };
 
+      // Register Custom Protocols
+      let mut protocol_ptrs = Vec::new();
       for (name, function) in attributes.custom_protocols {
-        let scheme_name = format!("{}URLSchemeHandler", name);
-        let cls = ClassDecl::new(&scheme_name, class!(NSObject));
-        let cls = match cls {
-          Some(mut cls) => {
-            cls.add_ivar::<*mut c_void>("function");
-            cls.add_ivar::<*mut c_char>("id");
-            cls.add_method(
-              sel!(webView:startURLSchemeTask:),
-              start_task as extern "C" fn(&Object, Sel, id, id),
-            );
-            cls.add_method(
-              sel!(webView:stopURLSchemeTask:),
-              stop_task as extern "C" fn(&Object, Sel, id, id),
-            );
-            cls.register()
-          }
-          None => Class::get(&scheme_name).expect("Failed to get the class definition"),
-        };
-        let handler: id = msg_send![cls, new];
+        let url_scheme_handler_cls = url_scheme_handler::create(&name);
+        let handler: *mut AnyObject = objc2::msg_send![url_scheme_handler_cls, new];
         let function = Box::into_raw(Box::new(function));
         protocol_ptrs.push(function);
 
-        (*handler).set_ivar("function", function as *mut _ as *mut c_void);
-        (*handler).set_ivar(
-          "id",
-          CString::new(webview_id.as_bytes()).unwrap().into_raw(),
-        );
+        let ivar = (*handler).class().instance_variable("function").unwrap();
+        let ivar_delegate = ivar.load_mut(&mut *handler);
+        *ivar_delegate = function as *mut _ as *mut c_void;
 
-        (*config)
-          .send_message::<(id, NSString), ()>(
-            sel!(setURLSchemeHandler:forURLScheme:),
-            (handler, NSString::new(&name)),
-          )
-          .map_err(|_| crate::Error::UrlSchemeRegisterError(name))?;
+        let ivar = (*handler).class().instance_variable("webview_id").unwrap();
+        let ivar_delegate: &mut *mut c_char = ivar.load_mut(&mut *handler);
+        *ivar_delegate = CString::new(webview_id.as_bytes()).unwrap().into_raw();
+
+        let set_result = objc2::exception::catch(AssertUnwindSafe(|| {
+          config.setURLSchemeHandler_forURLScheme(
+            Some(&*(handler.cast::<ProtocolObject<dyn WKURLSchemeHandler>>())),
+            &NSString::from_str(&name),
+          );
+        }));
+        if set_result.is_err() {
+          return Err(Error::UrlSchemeRegisterError(name));
+        }
       }
 
       // WebView and manager
-      let manager: id = msg_send![config, userContentController];
-      let cls = match ClassDecl::new("WryWebView", class!(WKWebView)) {
-        #[allow(unused_mut)]
-        Some(mut decl) => {
-          #[cfg(target_os = "macos")]
-          {
-            add_drag_drop_methods(&mut decl);
-            synthetic_mouse_events::setup(&mut decl);
-            decl.add_ivar::<bool>(ACCEPT_FIRST_MOUSE);
-            decl.add_method(
-              sel!(acceptsFirstMouse:),
-              accept_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
-            );
-            extern "C" fn accept_first_mouse(this: &Object, _sel: Sel, _event: id) -> BOOL {
-              unsafe {
-                let accept: bool = *this.get_ivar(ACCEPT_FIRST_MOUSE);
-                if accept {
-                  YES
-                } else {
-                  NO
-                }
-              }
-            }
-
-            // This is a temporary workaround for https://github.com/tauri-apps/tauri/issues/9426
-            // FIXME: When the webview is a child webview, performKeyEquivalent always return YES
-            // and stop propagating the event to the window, hence the menu shortcut won't be
-            // triggered. However, overriding this method also means the cmd+key event won't be
-            // handled in webview, which means the key cannot be listened by JavaScript.
-            if is_child {
-              decl.add_method(
-                sel!(performKeyEquivalent:),
-                key_equivalent as extern "C" fn(&mut Object, Sel, id) -> BOOL,
-              );
-              extern "C" fn key_equivalent(_this: &mut Object, _sel: Sel, _event: id) -> BOOL {
-                NO
-              }
-            }
-          }
-          decl.register()
-        }
-        _ => class!(WryWebView),
-      };
-      let webview: id = msg_send![cls, alloc];
+      let manager = config.userContentController();
+      let webview = mtm.alloc::<WryWebView>().set_ivars(WryWebViewIvars {
+        is_child,
+        #[cfg(target_os = "macos")]
+        drag_drop_handler: match attributes.drag_drop_handler {
+          Some(handler) => handler,
+          None => Box::new(|_| false),
+        },
+        #[cfg(target_os = "macos")]
+        accept_first_mouse: Bool::new(attributes.accept_first_mouse),
+        custom_protocol_task_ids: HashMap::new(),
+      });
 
-      let () = msg_send![config, setWebsiteDataStore: data_store];
-      let _preference: id = msg_send![config, preferences];
-      let _yes: id = msg_send![class!(NSNumber), numberWithBool:1];
+      config.setWebsiteDataStore(&data_store);
+      let _preference = config.preferences();
+      let _yes = NSNumber::numberWithBool(true);
 
       #[cfg(feature = "mac-proxy")]
       if let Some(proxy_config) = attributes.proxy_config {
         let proxy_config = match proxy_config {
           ProxyConfig::Http(endpoint) => {
             let nw_endpoint = nw_endpoint_t::try_from(endpoint).unwrap();
-            nw_proxy_config_create_http_connect(nw_endpoint, nil)
+            nw_proxy_config_create_http_connect(nw_endpoint, null_mut())
           }
           ProxyConfig::Socks5(endpoint) => {
             let nw_endpoint = nw_endpoint_t::try_from(endpoint).unwrap();
@@ -508,43 +267,40 @@ impl InnerWebView {
           }
         };
 
-        let proxies: id = msg_send![class!(NSArray), arrayWithObject: proxy_config];
-        let () = msg_send![data_store, setProxyConfigurations: proxies];
+        let proxies: Retained<objc2_foundation::NSArray<NSObject>> =
+          objc2_foundation::NSArray::arrayWithObject(&*proxy_config);
+        data_store.setValue_forKey(Some(&proxies), ns_string!("proxyConfigurations"));
       }
 
-      #[cfg(target_os = "macos")]
-      (*webview).set_ivar(ACCEPT_FIRST_MOUSE, attributes.accept_first_mouse);
-
-      let _: id = msg_send![_preference, setValue:_yes forKey:NSString::new("allowsPictureInPictureMediaPlayback")];
+      _preference.setValue_forKey(
+        Some(&_yes),
+        ns_string!("allowsPictureInPictureMediaPlayback"),
+      );
 
       #[cfg(target_os = "ios")]
-      let _: id = msg_send![config, setAllowsInlineMediaPlayback: YES];
+      config.setValue_forKey(Some(&_yes), ns_string!("allowsInlineMediaPlayback"));
 
       if attributes.autoplay {
-        let _: id = msg_send![config, setMediaTypesRequiringUserActionForPlayback:0];
+        config.setMediaTypesRequiringUserActionForPlayback(
+          WKAudiovisualMediaTypes::WKAudiovisualMediaTypeNone,
+        );
       }
 
-      #[cfg(target_os = "macos")]
-      let _: id = msg_send![_preference, setValue:_yes forKey:NSString::new("tabFocusesLinks")];
-
       #[cfg(feature = "transparent")]
       if attributes.transparent {
-        let no: id = msg_send![class!(NSNumber), numberWithBool:0];
+        let no = NSNumber::numberWithBool(false);
         // Equivalent Obj-C:
-        // [config setValue:@NO forKey:@"drawsBackground"];
-        let _: id = msg_send![config, setValue:no forKey:NSString::new("drawsBackground")];
+        config.setValue_forKey(Some(&no), ns_string!("drawsBackground"));
       }
 
       #[cfg(feature = "fullscreen")]
       // Equivalent Obj-C:
-      // [preference setValue:@YES forKey:@"fullScreenEnabled"];
-      let _: id = msg_send![_preference, setValue:_yes forKey:NSString::new("fullScreenEnabled")];
+      _preference.setValue_forKey(Some(&_yes), ns_string!("fullScreenEnabled"));
 
       #[cfg(target_os = "macos")]
-      {
-        use cocoa::appkit::NSWindow;
+      let webview = {
+        let window = ns_view.window().unwrap();
 
-        let window: id = msg_send![ns_view, window];
         let scale_factor = window.backingScaleFactor();
         let (x, y) = attributes
           .bounds
@@ -569,427 +325,147 @@ impl InnerWebView {
         });
 
         let frame = CGRect {
-          origin: window_position(if is_child { ns_view } else { webview }, x, y, h as f64),
+          origin: if is_child {
+            window_position(ns_view, x, y, h as f64)
+          } else {
+            CGPoint::new(x as f64, (0 - y - h as i32) as f64)
+          },
           size: CGSize::new(w as f64, h as f64),
         };
+        let webview: Retained<WryWebView> =
+          objc2::msg_send_id![super(webview), initWithFrame:frame configuration:&**config];
+        webview
+      };
+      #[cfg(target_os = "ios")]
+      let webview = {
+        let frame = ns_view.frame();
+        let webview: Retained<WryWebView> =
+          objc2::msg_send_id![super(webview), initWithFrame:frame configuration:&**config];
+        webview
+      };
 
-        let _: () = msg_send![webview, initWithFrame:frame configuration:config];
+      #[cfg(target_os = "macos")]
+      {
         if is_child {
           // fixed element
-          webview.setAutoresizingMask_(NSViewMinYMargin);
+          webview.setAutoresizingMask(NSAutoresizingMaskOptions::NSViewMinYMargin);
         } else {
           // Auto-resize
-          webview.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable);
+          webview.setAutoresizingMask(
+            NSAutoresizingMaskOptions::NSViewHeightSizable
+              | NSAutoresizingMaskOptions::NSViewWidthSizable,
+          );
         }
-      }
 
+        // allowsBackForwardNavigation
+        webview.setAllowsBackForwardNavigationGestures(attributes.back_forward_navigation_gestures);
+
+        // tabFocusesLinks
+        _preference.setValue_forKey(Some(&_yes), ns_string!("tabFocusesLinks"));
+      }
       #[cfg(target_os = "ios")]
       {
-        let frame: CGRect = msg_send![ns_view, frame];
         // set all autoresizingmasks
-        let () = msg_send![webview, setAutoresizingMask: 31];
-        let _: () = msg_send![webview, initWithFrame:frame configuration:config];
+        webview.setAutoresizingMask(UIViewAutoresizing::from_bits(31).unwrap());
+        // let () = msg_send![webview, setAutoresizingMask: 31];
 
         // disable scroll bounce by default
-        let scroll: id = msg_send![webview, scrollView];
-        let _: () = msg_send![scroll, setBounces: NO];
+        // https://developer.apple.com/documentation/webkit/wkwebview/1614784-scrollview?language=objc
+        // But not exist in objc2-web-kit
+        let scroll_view: Retained<UIScrollView> = objc2::msg_send_id![&webview, scrollView];
+        // let scroll_view: Retained<UIScrollView> = webview.ivars().scrollView; // FIXME: not test yet
+        scroll_view.setBounces(false)
       }
 
       if !attributes.visible {
-        let () = msg_send![webview, setHidden: YES];
+        webview.setHidden(true);
       }
 
       #[cfg(any(debug_assertions, feature = "devtools"))]
       if attributes.devtools {
-        let has_inspectable_property: BOOL =
-          msg_send![webview, respondsToSelector: sel!(setInspectable:)];
-        if has_inspectable_property == YES {
-          let _: () = msg_send![webview, setInspectable: YES];
+        let has_inspectable_property: bool =
+          NSObject::respondsToSelector(&webview, objc2::sel!(setInspectable:));
+        if has_inspectable_property {
+          webview.setInspectable(true);
         }
         // this cannot be on an `else` statement, it does not work on macOS :(
-        let dev = NSString::new("developerExtrasEnabled");
-        let _: id = msg_send![_preference, setValue:_yes forKey:dev];
-      }
-
-      // allowsBackForwardNavigation
-      #[cfg(target_os = "macos")]
-      {
-        let value = attributes.back_forward_navigation_gestures;
-        let _: () = msg_send![webview, setAllowsBackForwardNavigationGestures: value];
+        let dev = NSString::from_str("developerExtrasEnabled");
+        _preference.setValue_forKey(Some(&_yes), &dev);
       }
 
       // Message handler
-      let ipc_handler_ptr = if let Some(ipc_handler) = attributes.ipc_handler {
-        let cls = ClassDecl::new("WebViewDelegate", class!(NSObject));
-        let cls = match cls {
-          Some(mut cls) => {
-            cls.add_ivar::<*mut c_void>("function");
-            cls.add_method(
-              sel!(userContentController:didReceiveScriptMessage:),
-              did_receive as extern "C" fn(&Object, Sel, id, id),
-            );
-            cls.register()
-          }
-          None => class!(WebViewDelegate),
-        };
-        let handler: id = msg_send![cls, new];
-        let ipc_handler_ptr = Box::into_raw(Box::new(ipc_handler));
-
-        (*handler).set_ivar("function", ipc_handler_ptr as *mut _ as *mut c_void);
-        let ipc = NSString::new(IPC_MESSAGE_HANDLER_NAME);
-        let _: () = msg_send![manager, addScriptMessageHandler:handler name:ipc];
-        ipc_handler_ptr
+      let ipc_handler_delegate = if let Some(ipc_handler) = attributes.ipc_handler {
+        let delegate = WryWebViewDelegate::new(manager.clone(), ipc_handler, mtm);
+        Some(delegate)
       } else {
-        null_mut()
+        None
       };
 
       // Document title changed handler
-      let document_title_changed_handler = if let Some(document_title_changed_handler) =
-        attributes.document_title_changed_handler
-      {
-        let cls = ClassDecl::new("DocumentTitleChangedDelegate", class!(NSObject));
-        let cls = match cls {
-          Some(mut cls) => {
-            cls.add_ivar::<*mut c_void>("function");
-            cls.add_method(
-              sel!(observeValueForKeyPath:ofObject:change:context:),
-              observe_value_for_key_path as extern "C" fn(&Object, Sel, id, id, id, id),
-            );
-            extern "C" fn observe_value_for_key_path(
-              this: &Object,
-              _sel: Sel,
-              key_path: id,
-              of_object: id,
-              _change: id,
-              _context: id,
-            ) {
-              let key = NSString(key_path);
-              if key.to_str() == "title" {
-                unsafe {
-                  let function = this.get_ivar::<*mut c_void>("function");
-                  if !function.is_null() {
-                    let function = &mut *(*function as *mut Box<dyn Fn(String)>);
-                    let title: id = msg_send![of_object, title];
-                    (function)(NSString(title).to_str().to_string());
-                  }
-                }
-              }
-            }
-            cls.register()
-          }
-          None => class!(DocumentTitleChangedDelegate),
+      let document_title_changed_observer =
+        if let Some(handler) = attributes.document_title_changed_handler {
+          let delegate = DocumentTitleChangedObserver::new(webview.clone(), handler);
+          Some(delegate)
+        } else {
+          None
         };
 
-        let handler: id = msg_send![cls, new];
-        let document_title_changed_handler =
-          Box::into_raw(Box::new(document_title_changed_handler));
-
-        (*handler).set_ivar(
-          "function",
-          document_title_changed_handler as *mut _ as *mut c_void,
-        );
-
-        let _: () = msg_send![webview, addObserver:handler forKeyPath:NSString::new("title") options:0x01 context:nil ];
-
-        document_title_changed_handler
-      } else {
-        null_mut()
-      };
-
-      // Navigation handler
-      extern "C" fn navigation_policy(this: &Object, _: Sel, _: id, action: id, handler: id) {
-        unsafe {
-          // shouldPerformDownload is only available on macOS 11.3+
-          let can_download: BOOL =
-            msg_send![action, respondsToSelector: sel!(shouldPerformDownload)];
-          let should_download: BOOL = if can_download == YES {
-            msg_send![action, shouldPerformDownload]
-          } else {
-            NO
-          };
-          let request: id = msg_send![action, request];
-          let url: id = msg_send![request, URL];
-          let url: id = msg_send![url, absoluteString];
-          let url = NSString(url);
-          let target_frame: id = msg_send![action, targetFrame];
-          let is_main_frame: bool = msg_send![target_frame, isMainFrame];
-
-          let handler = handler as *mut block::Block<(NSInteger,), c_void>;
-
-          if should_download == YES {
-            let has_download_handler = this.get_ivar::<*mut c_void>("HasDownloadHandler");
-            if !has_download_handler.is_null() {
-              let has_download_handler = &mut *(*has_download_handler as *mut Box<bool>);
-              if **has_download_handler {
-                (*handler).call((2,));
-              } else {
-                (*handler).call((0,));
-              }
-            } else {
-              (*handler).call((0,));
-            }
-          } else {
-            let function = this.get_ivar::<*mut c_void>("navigation_policy_function");
-            if !function.is_null() {
-              let function = &mut *(*function as *mut Box<dyn for<'s> Fn(String, bool) -> bool>);
-              match (function)(url.to_str().to_string(), is_main_frame) {
-                true => (*handler).call((1,)),
-                false => (*handler).call((0,)),
-              };
-            } else {
-              (*handler).call((1,));
-            }
-          }
-        }
-      }
-
-      // Navigation handler
-      extern "C" fn navigation_policy_response(
-        this: &Object,
-        _: Sel,
-        _: id,
-        response: id,
-        handler: id,
-      ) {
-        unsafe {
-          let handler = handler as *mut block::Block<(NSInteger,), c_void>;
-          let can_show_mime_type: bool = msg_send![response, canShowMIMEType];
-
-          if !can_show_mime_type {
-            let has_download_handler = this.get_ivar::<*mut c_void>("HasDownloadHandler");
-            if !has_download_handler.is_null() {
-              let has_download_handler = &mut *(*has_download_handler as *mut Box<bool>);
-              if **has_download_handler {
-                (*handler).call((2,));
-                return;
-              }
-            }
-          }
-
-          (*handler).call((1,));
-        }
-      }
-
       let pending_scripts = Arc::new(Mutex::new(Some(Vec::new())));
-
-      let navigation_delegate_cls = match ClassDecl::new("WryNavigationDelegate", class!(NSObject))
+      let has_download_handler = attributes.download_started_handler.is_some();
+      // Download handler
+      let download_delegate = if attributes.download_started_handler.is_some()
+        || attributes.download_completed_handler.is_some()
       {
-        Some(mut cls) => {
-          cls.add_ivar::<*mut c_void>("pending_scripts");
-          cls.add_ivar::<*mut c_void>("HasDownloadHandler");
-          cls.add_method(
-            sel!(webView:decidePolicyForNavigationAction:decisionHandler:),
-            navigation_policy as extern "C" fn(&Object, Sel, id, id, id),
-          );
-          cls.add_method(
-            sel!(webView:decidePolicyForNavigationResponse:decisionHandler:),
-            navigation_policy_response as extern "C" fn(&Object, Sel, id, id, id),
-          );
-          add_download_methods(&mut cls);
-          add_navigation_mathods(&mut cls);
-          cls.register()
-        }
-        None => class!(WryNavigationDelegate),
-      };
-
-      let navigation_policy_handler: id = msg_send![navigation_delegate_cls, new];
-
-      (*navigation_policy_handler).set_ivar(
-        "pending_scripts",
-        Box::into_raw(Box::new(pending_scripts.clone())) as *mut c_void,
-      );
-
-      let (navigation_decide_policy_ptr, download_delegate) = if attributes
-        .navigation_handler
-        .is_some()
-        || attributes.new_window_req_handler.is_some()
-        || attributes.download_started_handler.is_some()
-      {
-        let function_ptr = {
-          let navigation_handler = attributes.navigation_handler;
-          let new_window_req_handler = attributes.new_window_req_handler;
-          Box::into_raw(Box::new(
-            Box::new(move |url: String, is_main_frame: bool| -> bool {
-              if is_main_frame {
-                navigation_handler
-                  .as_ref()
-                  .map_or(true, |navigation_handler| (navigation_handler)(url))
-              } else {
-                new_window_req_handler
-                  .as_ref()
-                  .map_or(true, |new_window_req_handler| (new_window_req_handler)(url))
-              }
-            }) as Box<dyn Fn(String, bool) -> bool>,
-          ))
-        };
-        (*navigation_policy_handler).set_ivar(
-          "navigation_policy_function",
-          function_ptr as *mut _ as *mut c_void,
+        let delegate = WryDownloadDelegate::new(
+          attributes.download_started_handler,
+          attributes.download_completed_handler,
+          mtm,
         );
-
-        let has_download_handler = Box::into_raw(Box::new(Box::new(
-          attributes.download_started_handler.is_some(),
-        )));
-        (*navigation_policy_handler).set_ivar(
-          "HasDownloadHandler",
-          has_download_handler as *mut _ as *mut c_void,
-        );
-
-        // Download handler
-        let download_delegate = if attributes.download_started_handler.is_some()
-          || attributes.download_completed_handler.is_some()
-        {
-          let cls = match ClassDecl::new("WryDownloadDelegate", class!(NSObject)) {
-            Some(mut cls) => {
-              cls.add_ivar::<*mut c_void>("started");
-              cls.add_ivar::<*mut c_void>("completed");
-              cls.add_method(
-                sel!(download:decideDestinationUsingResponse:suggestedFilename:completionHandler:),
-                download_policy as extern "C" fn(&Object, Sel, id, id, id, id),
-              );
-              cls.add_method(
-                sel!(downloadDidFinish:),
-                download_did_finish as extern "C" fn(&Object, Sel, id),
-              );
-              cls.add_method(
-                sel!(download:didFailWithError:resumeData:),
-                download_did_fail as extern "C" fn(&Object, Sel, id, id, id),
-              );
-              cls.register()
-            }
-            None => class!(WryDownloadDelegate),
-          };
-
-          let download_delegate: id = msg_send![cls, new];
-          if let Some(download_started_handler) = attributes.download_started_handler {
-            let download_started_ptr = Box::into_raw(Box::new(download_started_handler));
-            (*download_delegate).set_ivar("started", download_started_ptr as *mut _ as *mut c_void);
-          }
-          if let Some(download_completed_handler) = attributes.download_completed_handler {
-            let download_completed_ptr = Box::into_raw(Box::new(download_completed_handler));
-            (*download_delegate)
-              .set_ivar("completed", download_completed_ptr as *mut _ as *mut c_void);
-          }
-
-          set_download_delegate(navigation_policy_handler, download_delegate);
-
-          navigation_policy_handler
-        } else {
-          null_mut()
-        };
-
-        (function_ptr, download_delegate)
+        Some(delegate)
       } else {
-        (null_mut(), null_mut())
+        None
       };
 
-      let page_load_handler = set_navigation_methods(
-        navigation_policy_handler,
-        webview,
+      let navigation_policy_delegate = WryNavigationDelegate::new(
+        webview.clone(),
+        pending_scripts.clone(),
+        has_download_handler,
+        attributes.navigation_handler,
+        attributes.new_window_req_handler,
+        download_delegate.clone(),
         attributes.on_page_load_handler,
+        mtm,
       );
 
-      let _: () = msg_send![webview, setNavigationDelegate: navigation_policy_handler];
-
-      // File upload panel handler
-      extern "C" fn run_file_upload_panel(
-        _this: &Object,
-        _: Sel,
-        _webview: id,
-        open_panel_params: id,
-        _frame: id,
-        handler: id,
-      ) {
-        unsafe {
-          let handler = handler as *mut block::Block<(id,), c_void>;
-          let cls = class!(NSOpenPanel);
-          let open_panel: id = msg_send![cls, openPanel];
-          let _: () = msg_send![open_panel, setCanChooseFiles: YES];
-          let allow_multi: BOOL = msg_send![open_panel_params, allowsMultipleSelection];
-          let _: () = msg_send![open_panel, setAllowsMultipleSelection: allow_multi];
-          let allow_dir: BOOL = msg_send![open_panel_params, allowsDirectories];
-          let _: () = msg_send![open_panel, setCanChooseDirectories: allow_dir];
-          let ok: NSInteger = msg_send![open_panel, runModal];
-          if ok == 1 {
-            let url: id = msg_send![open_panel, URLs];
-            (*handler).call((url,));
-          } else {
-            (*handler).call((nil,));
-          }
-        }
-      }
-
-      extern "C" fn request_media_capture_permission(
-        _this: &Object,
-        _: Sel,
-        _webview: id,
-        _origin: id,
-        _frame: id,
-        _type: id,
-        decision_handler: id,
-      ) {
-        unsafe {
-          let decision_handler = decision_handler as *mut block::Block<(NSInteger,), c_void>;
-          //https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc
-          (*decision_handler).call((1,));
-        }
-      }
-
-      let ui_delegate = match ClassDecl::new("WebViewUIDelegate", class!(NSObject)) {
-        Some(mut ctl) => {
-          ctl.add_method(
-            sel!(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:),
-            run_file_upload_panel as extern "C" fn(&Object, Sel, id, id, id, id),
-          );
-
-          ctl.add_method(
-            sel!(webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:),
-            request_media_capture_permission as extern "C" fn(&Object, Sel, id, id, id, id, id),
-          );
+      let proto_navigation_policy_delegate =
+        ProtocolObject::from_ref(navigation_policy_delegate.as_ref());
+      webview.setNavigationDelegate(Some(proto_navigation_policy_delegate));
 
-          ctl.register()
-        }
-        None => class!(WebViewUIDelegate),
-      };
-      let ui_delegate: id = msg_send![ui_delegate, new];
-      let _: () = msg_send![webview, setUIDelegate: ui_delegate];
-
-      // File drop handling
-      #[cfg(target_os = "macos")]
-      let drag_drop_ptr = match attributes.drag_drop_handler {
-        // if we have a drag_drop_handler defined, use the defined handler
-        Some(drag_drop_handler) => set_drag_drop_handler(webview, drag_drop_handler),
-        // prevent panic by using a blank handler
-        None => set_drag_drop_handler(webview, Box::new(|_| false)),
-      };
+      let ui_delegate: Retained<WryWebViewUIDelegate> = WryWebViewUIDelegate::new(mtm);
+      let proto_ui_delegate = ProtocolObject::from_ref(ui_delegate.as_ref());
+      webview.setUIDelegate(Some(proto_ui_delegate));
 
       // ns window is required for the print operation
       #[cfg(target_os = "macos")]
       {
-        let ns_window: id = msg_send![ns_view, window];
-
-        let can_set_titlebar_style: BOOL = msg_send![
-          ns_window,
-          respondsToSelector: sel!(setTitlebarSeparatorStyle:)
-        ];
-        if can_set_titlebar_style == YES {
-          // `1` means `none`, see https://developer.apple.com/documentation/appkit/nstitlebarseparatorstyle/none
-          let () = msg_send![ns_window, setTitlebarSeparatorStyle: 1];
+        let ns_window = ns_view.window().unwrap();
+        let can_set_titlebar_style =
+          ns_window.respondsToSelector(objc2::sel!(setTitlebarSeparatorStyle:));
+        if can_set_titlebar_style {
+          ns_window.setTitlebarSeparatorStyle(NSTitlebarSeparatorStyle::None);
         }
       }
 
       let w = Self {
         id: webview_id,
-        webview,
-        manager,
+        webview: webview.clone(),
+        manager: manager.clone(),
         pending_scripts,
-        ipc_handler_ptr,
-        document_title_changed_handler,
-        navigation_decide_policy_ptr,
-        #[cfg(target_os = "macos")]
-        drag_drop_ptr,
-        page_load_handler,
+        ipc_handler_delegate,
+        document_title_changed_observer,
+        navigation_policy_delegate,
         download_delegate,
+        ui_delegate,
         protocol_ptrs,
         is_child,
       };
@@ -1020,50 +496,31 @@ r#"Object.defineProperty(window, 'ipc', {
       #[cfg(target_os = "macos")]
       {
         if is_child {
-          let _: () = msg_send![ns_view, addSubview: webview];
+          ns_view.addSubview(&webview);
         } else {
-          let parent_view_cls = match ClassDecl::new("WryWebViewParent", class!(NSView)) {
-            Some(mut decl) => {
-              decl.add_method(
-                sel!(keyDown:),
-                key_down as extern "C" fn(&mut Object, Sel, id),
-              );
-
-              extern "C" fn key_down(_this: &mut Object, _sel: Sel, event: id) {
-                unsafe {
-                  let app = cocoa::appkit::NSApp();
-                  let menu: id = msg_send![app, mainMenu];
-                  let () = msg_send![menu, performKeyEquivalent: event];
-                }
-              }
-
-              decl.register()
-            }
-            None => class!(NSView),
-          };
-
-          let parent_view: id = msg_send![parent_view_cls, alloc];
-          let _: () = msg_send![parent_view, init];
-          parent_view.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable);
-          let _: () = msg_send![parent_view, addSubview: webview];
+          let parent_view = WryWebViewParent::new(mtm);
+          parent_view.setAutoresizingMask(
+            NSAutoresizingMaskOptions::NSViewHeightSizable
+              | NSAutoresizingMaskOptions::NSViewWidthSizable,
+          );
+          parent_view.addSubview(&webview.clone());
 
           // inject the webview into the window
-          let ns_window: id = msg_send![ns_view, window];
+          let ns_window = ns_view.window().unwrap();
           // Tell the webview receive keyboard events in the window.
           // See https://github.com/tauri-apps/wry/issues/739
-          let _: () = msg_send![ns_window, setContentView: parent_view];
-          let _: () = msg_send![ns_window, makeFirstResponder: webview];
+          ns_window.setContentView(Some(&parent_view));
+          ns_window.makeFirstResponder(Some(&webview));
         }
 
         // make sure the window is always on top when we create a new webview
-        let app_class = class!(NSApplication);
-        let app: id = msg_send![app_class, sharedApplication];
-        let _: () = msg_send![app, activateIgnoringOtherApps: YES];
+        let app = NSApplication::sharedApplication(mtm);
+        NSApplication::activate(&app);
       }
 
       #[cfg(target_os = "ios")]
       {
-        let _: () = msg_send![ns_view, addSubview: webview];
+        ns_view.addSubview(&webview);
       }
 
       Ok(w)
@@ -1075,7 +532,7 @@ r#"Object.defineProperty(window, 'ipc', {
   }
 
   pub fn url(&self) -> crate::Result<String> {
-    url_from_webview(self.webview)
+    url_from_webview(&self.webview)
   }
 
   pub fn eval(&self, js: &str, callback: Option<impl Fn(String) + Send + 'static>) -> Result<()> {
@@ -1089,36 +546,45 @@ r#"Object.defineProperty(window, 'ipc', {
 
         // we need to check if the callback exists outside the handler otherwise it's a segfault
         if let Some(callback) = callback {
-          let handler = block::ConcreteBlock::new(move |val: id, _err: id| {
+          let handler = block2::RcBlock::new(move |val: *mut AnyObject, _err: *mut NSError| {
             #[cfg(feature = "tracing")]
             span.lock().unwrap().take();
 
             let mut result = String::new();
 
-            if val != nil {
-              let serializer = class!(NSJSONSerialization);
-              let json_ns_data: NSData = msg_send![serializer, dataWithJSONObject:val options:NS_JSON_WRITING_FRAGMENTS_ALLOWED error:nil];
-              let json_string = NSString::from(json_ns_data);
-
-              result = json_string.to_str().to_string();
+            if !val.is_null() {
+              let json_ns_data = NSJSONSerialization::dataWithJSONObject_options_error(
+                &*val,
+                objc2_foundation::NSJSONWritingOptions::NSJSONWritingFragmentsAllowed,
+              )
+              .unwrap();
+              let json_string = NSString::alloc();
+              let json_string =
+                NSString::initWithData_encoding(json_string, &json_ns_data, NSUTF8StringEncoding)
+                  .unwrap();
+
+              result = json_string.to_string();
             }
 
             callback(result);
-          }).copy();
+          });
 
-          let _: () =
-            msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:handler];
+          self
+            .webview
+            .evaluateJavaScript_completionHandler(&NSString::from_str(js), Some(&handler));
         } else {
           #[cfg(feature = "tracing")]
-          let handler = block::ConcreteBlock::new(move |_val: id, _err: id| {
-            span.lock().unwrap().take();
-          })
-          .copy();
+          let handler = Some(block2::RcBlock::new(
+            move |_val: *mut AnyObject, _err: *mut NSError| {
+              span.lock().unwrap().take();
+            },
+          ));
           #[cfg(not(feature = "tracing"))]
-          let handler = null::<*const c_void>();
+          let handler: Option<block2::RcBlock<dyn Fn(*mut AnyObject, *mut NSError)>> = None;
 
-          let _: () =
-            msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:handler];
+          self
+            .webview
+            .evaluateJavaScript_completionHandler(&NSString::from_str(js), handler.as_deref());
         }
       }
     }
@@ -1128,14 +594,16 @@ r#"Object.defineProperty(window, 'ipc', {
 
   fn init(&self, js: &str) {
     // Safety: objc runtime calls are unsafe
-    // Equivalent Obj-C:
-    // [manager addUserScript:[[WKUserScript alloc] initWithSource:[NSString stringWithUTF8String:js.c_str()] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]]
     unsafe {
-      let userscript: id = msg_send![class!(WKUserScript), alloc];
-      let script: id =
+      let userscript = WKUserScript::alloc();
       // TODO: feature to allow injecting into subframes
-        msg_send![userscript, initWithSource:NSString::new(js) injectionTime:0 forMainFrameOnly:1];
-      let _: () = msg_send![self.manager, addUserScript: script];
+      let script = WKUserScript::initWithSource_injectionTime_forMainFrameOnly(
+        userscript,
+        &NSString::from_str(js),
+        WKUserScriptInjectionTime::AtDocumentStart,
+        true,
+      );
+      self.manager.addUserScript(&script);
     }
   }
 
@@ -1154,12 +622,12 @@ r#"Object.defineProperty(window, 'ipc', {
 
   pub fn clear_all_browsing_data(&self) -> Result<()> {
     unsafe {
-      let config: id = msg_send![self.webview, configuration];
-      let store: id = msg_send![config, websiteDataStore];
-      let all_data_types: id = msg_send![class!(WKWebsiteDataStore), allWebsiteDataTypes];
-      let date: id = msg_send![class!(NSDate), dateWithTimeIntervalSince1970: 0.0];
-      let handler = block::ConcreteBlock::new(|| {}).copy();
-      let _: () = msg_send![store, removeDataOfTypes:all_data_types modifiedSince:date completionHandler:handler];
+      let config = self.webview.configuration();
+      let store = config.websiteDataStore();
+      let all_data_types = WKWebsiteDataStore::allWebsiteDataTypes();
+      let date = NSDate::dateWithTimeIntervalSince1970(0.0);
+      let handler = block2::RcBlock::new(|| {});
+      store.removeDataOfTypes_modifiedSince_completionHandler(&all_data_types, &date, &handler);
     }
     Ok(())
   }
@@ -1167,16 +635,16 @@ r#"Object.defineProperty(window, 'ipc', {
   fn navigate_to_url(&self, url: &str, headers: Option<http::HeaderMap>) -> crate::Result<()> {
     // Safety: objc runtime calls are unsafe
     unsafe {
-      let url: id = msg_send![class!(NSURL), URLWithString: NSString::new(url)];
-      let request: id = msg_send![class!(NSMutableURLRequest), requestWithURL: url];
+      let url = NSURL::URLWithString(&NSString::from_str(url)).unwrap();
+      let mut request = NSMutableURLRequest::requestWithURL(&url);
       if let Some(headers) = headers {
         for (name, value) in headers.iter() {
-          let key = NSString::new(name.as_str());
-          let value = NSString::new(value.to_str().unwrap_or_default());
-          let _: () = msg_send![request, addValue:value.as_ptr() forHTTPHeaderField:key.as_ptr()];
+          let key = NSString::from_str(name.as_str());
+          let value = NSString::from_str(value.to_str().unwrap_or_default());
+          request.addValue_forHTTPHeaderField(&value, &key);
         }
       }
-      let () = msg_send![self.webview, loadRequest: request];
+      self.webview.loadRequest(&request);
     }
 
     Ok(())
@@ -1185,13 +653,17 @@ r#"Object.defineProperty(window, 'ipc', {
   fn navigate_to_string(&self, html: &str) {
     // Safety: objc runtime calls are unsafe
     unsafe {
-      let () = msg_send![self.webview, loadHTMLString:NSString::new(html) baseURL:nil];
+      self
+        .webview
+        .loadHTMLString_baseURL(&NSString::from_str(html), None);
     }
   }
 
   fn set_user_agent(&self, user_agent: &str) {
     unsafe {
-      let () = msg_send![self.webview, setCustomUserAgent: NSString::new(user_agent)];
+      self
+        .webview
+        .setCustomUserAgent(Some(&NSString::from_str(user_agent)));
     }
   }
 
@@ -1199,29 +671,36 @@ r#"Object.defineProperty(window, 'ipc', {
     self.print_with_options(&PrintOptions::default())
   }
 
-  pub fn print_with_options(&self, options: &PrintOptions) -> crate::Result<()> {
+  pub fn print_with_options(&self, _options: &PrintOptions) -> crate::Result<()> {
     // Safety: objc runtime calls are unsafe
     #[cfg(target_os = "macos")]
     unsafe {
-      let can_print: BOOL = msg_send![
-        self.webview,
-        respondsToSelector: sel!(printOperationWithPrintInfo:)
-      ];
-      if can_print == YES {
+      let can_print = self
+        .webview
+        .respondsToSelector(objc2::sel!(printOperationWithPrintInfo:));
+      if can_print {
         // Create a shared print info
-        let print_info: id = msg_send![class!(NSPrintInfo), sharedPrintInfo];
-        let print_info: id = msg_send![print_info, init];
-        let () = msg_send![print_info, setTopMargin:CGFloat::from(options.margins.top)];
-        let () = msg_send![print_info, setRightMargin:CGFloat::from(options.margins.right)];
-        let () = msg_send![print_info, setBottomMargin:CGFloat::from(options.margins.bottom)];
-        let () = msg_send![print_info, setLeftMargin:CGFloat::from(options.margins.left)];
+        let print_info = objc2_app_kit::NSPrintInfo::sharedPrintInfo();
+        // let print_info: id = msg_send![print_info, init];
+        print_info.setTopMargin(_options.margins.top.into());
+        print_info.setRightMargin(_options.margins.right.into());
+        print_info.setBottomMargin(_options.margins.bottom.into());
+        print_info.setLeftMargin(_options.margins.left.into());
+
         // Create new print operation from the webview content
-        let print_operation: id = msg_send![self.webview, printOperationWithPrintInfo: print_info];
+        let print_operation = self.webview.printOperationWithPrintInfo(&print_info);
+
         // Allow the modal to detach from the current thread and be non-blocker
-        let () = msg_send![print_operation, setCanSpawnSeparateThread: YES];
+        print_operation.setCanSpawnSeparateThread(true);
+
         // Launch the modal
-        let window: id = msg_send![self.webview, window];
-        let () = msg_send![print_operation, runOperationModalForWindow: window delegate: null::<*const c_void>() didRunSelector: null::<*const c_void>() contextInfo: null::<*const c_void>()];
+        let window = self.webview.window().unwrap();
+        print_operation.runOperationModalForWindow_delegate_didRunSelector_contextInfo(
+          &window,
+          None,
+          None,
+          null_mut(),
+        )
       }
     }
 
@@ -1233,8 +712,8 @@ r#"Object.defineProperty(window, 'ipc', {
     #[cfg(target_os = "macos")]
     unsafe {
       // taken from <https://github.com/WebKit/WebKit/blob/784f93cb80a386c29186c510bba910b67ce3adc1/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm#L1939>
-      let tool: id = msg_send![self.webview, _inspector];
-      let _: id = msg_send![tool, show];
+      let tool: Retained<AnyObject> = objc2::msg_send_id![&self.webview, _inspector];
+      let () = objc2::msg_send![&tool, show];
     }
   }
 
@@ -1243,8 +722,8 @@ r#"Object.defineProperty(window, 'ipc', {
     #[cfg(target_os = "macos")]
     unsafe {
       // taken from <https://github.com/WebKit/WebKit/blob/784f93cb80a386c29186c510bba910b67ce3adc1/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm#L1939>
-      let tool: id = msg_send![self.webview, _inspector];
-      let _: id = msg_send![tool, close];
+      let tool: Retained<AnyObject> = objc2::msg_send_id![&self.webview, _inspector];
+      let () = objc2::msg_send![&tool, close];
     }
   }
 
@@ -1253,9 +732,9 @@ r#"Object.defineProperty(window, 'ipc', {
     #[cfg(target_os = "macos")]
     unsafe {
       // taken from <https://github.com/WebKit/WebKit/blob/784f93cb80a386c29186c510bba910b67ce3adc1/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm#L1939>
-      let tool: id = msg_send![self.webview, _inspector];
-      let is_visible: objc::runtime::BOOL = msg_send![tool, isVisible];
-      is_visible == objc::runtime::YES
+      let tool: Retained<AnyObject> = objc2::msg_send_id![&self.webview, _inspector];
+      let is_visible: bool = objc2::msg_send![&tool, isVisible];
+      is_visible
     }
     #[cfg(not(target_os = "macos"))]
     false
@@ -1263,7 +742,7 @@ r#"Object.defineProperty(window, 'ipc', {
 
   pub fn zoom(&self, scale_factor: f64) -> crate::Result<()> {
     unsafe {
-      let _: () = msg_send![self.webview, setPageZoom: scale_factor];
+      self.webview.setPageZoom(scale_factor);
     }
 
     Ok(())
@@ -1274,10 +753,11 @@ r#"Object.defineProperty(window, 'ipc', {
   }
 
   pub fn bounds(&self) -> crate::Result<Rect> {
+    #[allow(unused_unsafe)]
     unsafe {
-      let parent: id = msg_send![self.webview, superview];
-      let parent_frame: CGRect = msg_send![parent, frame];
-      let webview_frame: CGRect = msg_send![self.webview, frame];
+      let parent = self.webview.superview().unwrap();
+      let parent_frame = parent.frame();
+      let webview_frame = self.webview.frame();
 
       Ok(Rect {
         position: LogicalPosition::new(
@@ -1293,19 +773,18 @@ r#"Object.defineProperty(window, 'ipc', {
   pub fn set_bounds(&self, #[allow(unused)] bounds: Rect) -> crate::Result<()> {
     #[cfg(target_os = "macos")]
     if self.is_child {
-      use cocoa::appkit::NSWindow;
-
-      let window: id = unsafe { msg_send![self.webview, window] };
-      let scale_factor = unsafe { window.backingScaleFactor() };
+      let window = self.webview.window().unwrap();
+      let scale_factor = window.backingScaleFactor();
       let (x, y) = bounds.position.to_logical::<f64>(scale_factor).into();
       let (width, height) = bounds.size.to_logical::<i32>(scale_factor).into();
 
       unsafe {
+        let parent_view = self.webview.superview().unwrap();
         let frame = CGRect {
-          origin: window_position(msg_send![self.webview, superview], x, y, height),
+          origin: window_position(&parent_view, x, y, height),
           size: CGSize::new(width, height),
         };
-        let () = msg_send![self.webview, setFrame: frame];
+        self.webview.setFrame(frame);
       }
     }
 
@@ -1313,44 +792,41 @@ r#"Object.defineProperty(window, 'ipc', {
   }
 
   pub fn set_visible(&self, visible: bool) -> Result<()> {
-    unsafe {
-      let () = msg_send![self.webview, setHidden: !visible];
-    }
-
+    self.webview.setHidden(!visible);
     Ok(())
   }
 
   pub fn focus(&self) -> Result<()> {
-    unsafe {
-      let window: id = msg_send![self.webview, window];
-      let _: () = msg_send![window, makeFirstResponder: self.webview];
+    #[cfg(target_os = "macos")]
+    {
+      let window = self.webview.window().unwrap();
+      window.makeFirstResponder(Some(&self.webview));
     }
-
     Ok(())
   }
 
   #[cfg(target_os = "macos")]
-  pub(crate) fn reparent(&self, window: id) -> crate::Result<()> {
+  pub(crate) fn reparent(&self, window: *mut NSWindow) -> crate::Result<()> {
     unsafe {
-      let content_view: id = msg_send![window, contentView];
-      let _: () = msg_send![content_view, addSubview: self.webview];
+      let content_view = (*window).contentView().unwrap();
+      content_view.addSubview(&self.webview);
     }
 
     Ok(())
   }
 }
 
-pub fn url_from_webview(webview: id) -> Result<String> {
-  let url_obj: *mut Object = unsafe { msg_send![webview, URL] };
-  let absolute_url: *mut Object = unsafe { msg_send![url_obj, absoluteString] };
+pub fn url_from_webview(webview: &WKWebView) -> Result<String> {
+  let url_obj = unsafe { webview.URL().unwrap() };
+  let absolute_url = unsafe { url_obj.absoluteString().unwrap() };
 
   let bytes = {
-    let bytes: *const c_char = unsafe { msg_send![absolute_url, UTF8String] };
+    let bytes: *const c_char = absolute_url.UTF8String();
     bytes as *const u8
   };
 
   // 4 represents utf8 encoding
-  let len = unsafe { msg_send![absolute_url, lengthOfBytesUsingEncoding: 4] };
+  let len = absolute_url.lengthOfBytesUsingEncoding(4);
   let bytes = unsafe { std::slice::from_raw_parts(bytes, len) };
 
   std::str::from_utf8(bytes)
@@ -1360,15 +836,15 @@ pub fn url_from_webview(webview: id) -> Result<String> {
 
 pub fn platform_webview_version() -> Result<String> {
   unsafe {
-    let bundle: id =
-      msg_send![class!(NSBundle), bundleWithIdentifier: NSString::new("com.apple.WebKit")];
-    let dict: id = msg_send![bundle, infoDictionary];
-    let webkit_version: id = msg_send![dict, objectForKey: NSString::new("CFBundleVersion")];
-
-    let nsstring = NSString(webkit_version);
+    let bundle = NSBundle::bundleWithIdentifier(&NSString::from_str("com.apple.WebKit")).unwrap();
+    let dict = bundle.infoDictionary().unwrap();
+    let webkit_version = dict
+      .objectForKey(&NSString::from_str("CFBundleVersion"))
+      .unwrap();
+    let webkit_version = Retained::cast::<NSString>(webkit_version);
 
-    let () = msg_send![bundle, unload];
-    Ok(nsstring.to_str().to_string())
+    bundle.unload();
+    Ok(webkit_version.to_string())
   }
 }
 
@@ -1378,30 +854,13 @@ impl Drop for InnerWebView {
 
     // We need to drop handler closures here
     unsafe {
-      if !self.ipc_handler_ptr.is_null() {
-        drop(Box::from_raw(self.ipc_handler_ptr));
-
-        let ipc = NSString::new(IPC_MESSAGE_HANDLER_NAME);
-        let _: () = msg_send![self.manager, removeScriptMessageHandlerForName: ipc];
-      }
-
-      if !self.document_title_changed_handler.is_null() {
-        drop(Box::from_raw(self.document_title_changed_handler));
-      }
-
-      if !self.navigation_decide_policy_ptr.is_null() {
-        drop(Box::from_raw(self.navigation_decide_policy_ptr));
-      }
-
-      drop_navigation_methods(self);
-
-      #[cfg(target_os = "macos")]
-      if !self.drag_drop_ptr.is_null() {
-        drop(Box::from_raw(self.drag_drop_ptr));
-      }
-
-      if !self.download_delegate.is_null() {
-        self.download_delegate.drop_in_place();
+      if let Some(ipc_handler) = self.ipc_handler_delegate.take() {
+        let ipc = NSString::from_str(IPC_MESSAGE_HANDLER_NAME);
+        // this will decrease the retain count of the ipc handler and trigger the drop
+        ipc_handler
+          .ivars()
+          .controller
+          .removeScriptMessageHandlerForName(&ipc);
       }
 
       for ptr in self.protocol_ptrs.iter() {
@@ -1411,91 +870,18 @@ impl Drop for InnerWebView {
       }
 
       // Remove webview from window's NSView before dropping.
-      let () = msg_send![self.webview, removeFromSuperview];
-      let _: Id<_> = Id::from_retained_ptr(self.webview);
-      let _: Id<_> = Id::from_retained_ptr(self.manager);
-    }
-  }
-}
-
-const UTF8_ENCODING: usize = 4;
-
-struct NSUUID(id);
-
-impl NSUUID {
-  fn new(data: &[u8; 16]) -> Self {
-    NSUUID(unsafe {
-      let ns_uuid: id = msg_send![class!(NSUUID), alloc];
-      let ns_uuid: id = msg_send![ns_uuid, initWithUUIDBytes:data.as_ptr()];
-
-      let _: () = msg_send![ns_uuid, autorelease];
-
-      ns_uuid
-    })
-  }
-}
-
-struct NSString(id);
-
-impl NSString {
-  fn new(s: &str) -> Self {
-    // Safety: objc runtime calls are unsafe
-    NSString(unsafe {
-      let ns_string: id = msg_send![class!(NSString), alloc];
-      let ns_string: id = msg_send![ns_string,
-                            initWithBytes:s.as_ptr()
-                            length:s.len()
-                            encoding:UTF8_ENCODING];
-
-      // The thing is allocated in rust, the thing must be set to autorelease in rust to relinquish control
-      // or it can not be released correctly in OC runtime
-      let _: () = msg_send![ns_string, autorelease];
-
-      ns_string
-    })
-  }
-
-  fn to_str(&self) -> &str {
-    unsafe {
-      let bytes: *const c_char = msg_send![self.0, UTF8String];
-      let len = msg_send![self.0, lengthOfBytesUsingEncoding: UTF8_ENCODING];
-      let bytes = slice::from_raw_parts(bytes as *const u8, len);
-      str::from_utf8_unchecked(bytes)
-    }
-  }
-
-  #[allow(dead_code)] // only used when `mac-proxy` feature is enabled
-  fn to_cstr(&self) -> *const c_char {
-    unsafe {
-      let utf_8_string = msg_send![self.0, UTF8String];
-      utf_8_string
+      self.webview.removeFromSuperview();
+      self.webview.retain();
+      self.manager.retain();
     }
   }
-
-  fn as_ptr(&self) -> id {
-    self.0
-  }
 }
 
-impl From<NSData> for NSString {
-  fn from(value: NSData) -> Self {
-    Self(unsafe {
-      let ns_string: id = msg_send![class!(NSString), alloc];
-      let ns_string: id = msg_send![ns_string, initWithData:value encoding:UTF8_ENCODING];
-      let _: () = msg_send![ns_string, autorelease];
-
-      ns_string
-    })
-  }
-}
-
-#[allow(dead_code)] // rustc complains `id` is unused, but it is actually used from Objective-C
-struct NSData(id);
-
 /// Converts from wry screen-coordinates to macOS screen-coordinates.
 /// wry: top-left is (0, 0) and y increasing downwards
 /// macOS: bottom-left is (0, 0) and y increasing upwards
-unsafe fn window_position(view: id, x: i32, y: i32, height: f64) -> CGPoint {
-  let frame: CGRect = msg_send![view, frame];
+#[allow(dead_code)]
+unsafe fn window_position(view: &NSView, x: i32, y: i32, height: f64) -> CGPoint {
+  let frame: CGRect = view.frame();
   CGPoint::new(x as f64, frame.size.height - y as f64 - height)
 }
diff --git a/src/wkwebview/navigation.rs b/src/wkwebview/navigation.rs
index 567ce4ce7..9947412b4 100644
--- a/src/wkwebview/navigation.rs
+++ b/src/wkwebview/navigation.rs
@@ -1,86 +1,106 @@
-use std::{
-  ffi::c_void,
-  ptr::{null, null_mut},
-  sync::{Arc, Mutex},
+use objc2::DeclaredClass;
+use objc2_foundation::{NSObjectProtocol, NSString};
+use objc2_web_kit::{
+  WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationResponse,
+  WKNavigationResponsePolicy,
 };
 
-use cocoa::base::id;
-use objc::{
-  declare::ClassDecl,
-  runtime::{Object, Sel},
-};
+#[cfg(target_os = "ios")]
+use crate::wkwebview::ios::WKWebView::WKWebView;
+#[cfg(target_os = "macos")]
+use objc2_web_kit::WKWebView;
 
-use super::{url_from_webview, InnerWebView, NSString};
 use crate::PageLoadEvent;
 
-extern "C" fn did_commit_navigation(this: &Object, _: Sel, webview: id, _navigation: id) {
+use super::class::wry_navigation_delegate::WryNavigationDelegate;
+
+pub(crate) fn did_commit_navigation(
+  this: &WryNavigationDelegate,
+  webview: &WKWebView,
+  _navigation: &WKNavigation,
+) {
   unsafe {
     // Call on_load_handler
-    let on_page_load = this.get_ivar::<*mut c_void>("on_page_load_function");
-    if !on_page_load.is_null() {
-      let on_page_load = &mut *(*on_page_load as *mut Box<dyn Fn(PageLoadEvent)>);
+    if let Some(on_page_load) = &this.ivars().on_page_load_handler {
       on_page_load(PageLoadEvent::Started);
     }
 
     // Inject scripts
-    let pending_scripts_ptr: *mut c_void = *this.get_ivar("pending_scripts");
-    let pending_scripts = &(*(pending_scripts_ptr as *mut Arc<Mutex<Option<Vec<String>>>>));
-    let mut pending_scripts_ = pending_scripts.lock().unwrap();
-    if let Some(pending_scripts) = &*pending_scripts_ {
-      for script in pending_scripts {
-        let _: id = msg_send![webview, evaluateJavaScript:NSString::new(script) completionHandler:null::<*const c_void>()];
+    let mut pending_scripts = this.ivars().pending_scripts.lock().unwrap();
+    if let Some(scripts) = &*pending_scripts {
+      for script in scripts {
+        webview.evaluateJavaScript_completionHandler(&NSString::from_str(script), None);
       }
-      *pending_scripts_ = None;
+      *pending_scripts = None;
     }
   }
 }
 
-extern "C" fn did_finish_navigation(this: &Object, _: Sel, _webview: id, _navigation: id) {
-  unsafe {
-    // Call on_load_handler
-    let on_page_load = this.get_ivar::<*mut c_void>("on_page_load_function");
-    if !on_page_load.is_null() {
-      let on_page_load = &mut *(*on_page_load as *mut Box<dyn Fn(PageLoadEvent)>);
-      on_page_load(PageLoadEvent::Finished);
-    }
+pub(crate) fn did_finish_navigation(
+  this: &WryNavigationDelegate,
+  _webview: &WKWebView,
+  _navigation: &WKNavigation,
+) {
+  if let Some(on_page_load) = &this.ivars().on_page_load_handler {
+    on_page_load(PageLoadEvent::Finished);
   }
 }
 
-pub(crate) unsafe fn add_navigation_mathods(cls: &mut ClassDecl) {
-  cls.add_ivar::<*mut c_void>("navigation_policy_function");
-  cls.add_ivar::<*mut c_void>("on_page_load_function");
-
-  cls.add_method(
-    sel!(webView:didFinishNavigation:),
-    did_finish_navigation as extern "C" fn(&Object, Sel, id, id),
-  );
-  cls.add_method(
-    sel!(webView:didCommitNavigation:),
-    did_commit_navigation as extern "C" fn(&Object, Sel, id, id),
-  );
-}
+// Navigation handler
+pub(crate) fn navigation_policy(
+  this: &WryNavigationDelegate,
+  _webview: &WKWebView,
+  action: &WKNavigationAction,
+  handler: &block2::Block<dyn Fn(WKNavigationActionPolicy)>,
+) {
+  unsafe {
+    // shouldPerformDownload is only available on macOS 11.3+
+    let can_download = action.respondsToSelector(objc2::sel!(shouldPerformDownload));
+    let should_download: bool = if can_download {
+      action.shouldPerformDownload()
+    } else {
+      false
+    };
+    let request = action.request();
+    let url = request.URL().unwrap().absoluteString().unwrap();
+    let target_frame = action.targetFrame();
+    let is_main_frame = target_frame.map_or(false, |frame| frame.isMainFrame());
 
-pub(crate) unsafe fn drop_navigation_methods(inner: &mut InnerWebView) {
-  if !inner.page_load_handler.is_null() {
-    drop(Box::from_raw(inner.page_load_handler))
+    if should_download {
+      let has_download_handler = this.ivars().has_download_handler;
+      if has_download_handler {
+        (*handler).call((WKNavigationActionPolicy::Download,));
+      } else {
+        (*handler).call((WKNavigationActionPolicy::Cancel,));
+      }
+    } else {
+      let function = &this.ivars().navigation_policy_function;
+      match function(url.to_string(), is_main_frame) {
+        true => (*handler).call((WKNavigationActionPolicy::Allow,)),
+        false => (*handler).call((WKNavigationActionPolicy::Cancel,)),
+      };
+    }
   }
 }
 
-pub(crate) unsafe fn set_navigation_methods(
-  navigation_policy_handler: *mut Object,
-  webview: id,
-  on_page_load_handler: Option<Box<dyn Fn(PageLoadEvent, String)>>,
-) -> *mut Box<dyn Fn(PageLoadEvent)> {
-  if let Some(on_page_load_handler) = on_page_load_handler {
-    let on_page_load_handler = Box::into_raw(Box::new(Box::new(move |event| {
-      on_page_load_handler(event, url_from_webview(webview).unwrap_or_default());
-    }) as Box<dyn Fn(PageLoadEvent)>));
-    (*navigation_policy_handler).set_ivar(
-      "on_page_load_function",
-      on_page_load_handler as *mut _ as *mut c_void,
-    );
-    on_page_load_handler
-  } else {
-    null_mut()
+// Navigation handler
+pub(crate) fn navigation_policy_response(
+  this: &WryNavigationDelegate,
+  _webview: &WKWebView,
+  response: &WKNavigationResponse,
+  handler: &block2::Block<dyn Fn(WKNavigationResponsePolicy)>,
+) {
+  unsafe {
+    let can_show_mime_type = response.canShowMIMEType();
+
+    if !can_show_mime_type {
+      let has_download_handler = this.ivars().has_download_handler;
+      if has_download_handler {
+        (*handler).call((WKNavigationResponsePolicy::Download,));
+        return;
+      }
+    }
+
+    (*handler).call((WKNavigationResponsePolicy::Allow,));
   }
 }
diff --git a/src/wkwebview/proxy.rs b/src/wkwebview/proxy.rs
index 08290d167..b55ec6475 100644
--- a/src/wkwebview/proxy.rs
+++ b/src/wkwebview/proxy.rs
@@ -2,46 +2,21 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use cocoa::base::nil;
-use std::ffi::c_char;
+use objc2_foundation::NSObject;
+use std::ffi::{c_char, CString};
 
 use crate::{proxy::ProxyEndpoint, Error};
 
-use super::NSString;
-
-#[allow(non_camel_case_types)]
-pub type nw_endpoint_t = *mut objc::runtime::Object;
 #[allow(non_camel_case_types)]
-pub type nw_relay_hop_t = *mut objc::runtime::Object;
+pub type nw_endpoint_t = *mut NSObject;
 #[allow(non_camel_case_types)]
-pub type nw_protocol_options_t = *mut objc::runtime::Object;
+pub type nw_protocol_options_t = *mut NSObject;
 #[allow(non_camel_case_types)]
-pub type nw_proxy_config_t = *mut objc::runtime::Object;
+pub type nw_proxy_config_t = *mut NSObject;
 
 #[link(name = "Network", kind = "framework")]
 extern "C" {
-  #[allow(dead_code)]
-  fn nw_endpoint_create_url(url: *const c_char) -> nw_endpoint_t;
-  #[allow(dead_code)]
-  fn nw_endpoint_get_url(endpoint: nw_endpoint_t) -> *const c_char;
   fn nw_endpoint_create_host(host: *const c_char, port: *const c_char) -> nw_endpoint_t;
-  #[allow(dead_code)]
-  fn nw_proxy_config_set_username_and_password(
-    proxy_config: nw_proxy_config_t,
-    username: *const c_char,
-    password: *const c_char,
-  );
-  #[allow(dead_code)]
-  fn nw_relay_hop_create(
-    http3_relay_endpoint: nw_endpoint_t,
-    http2_relay_endpoint: nw_endpoint_t,
-    relay_tls_options: nw_protocol_options_t,
-  ) -> nw_relay_hop_t;
-  #[allow(dead_code)]
-  fn nw_proxy_config_create_relay(
-    first_hop: nw_relay_hop_t,
-    second_hop: nw_relay_hop_t,
-  ) -> nw_proxy_config_t;
   pub fn nw_proxy_config_create_socksv5(proxy_endpoint: nw_endpoint_t) -> nw_proxy_config_t;
   pub fn nw_proxy_config_create_http_connect(
     proxy_endpoint: nw_endpoint_t,
@@ -53,14 +28,16 @@ impl TryFrom<ProxyEndpoint> for nw_endpoint_t {
   type Error = Error;
   fn try_from(endpoint: ProxyEndpoint) -> Result<Self, Error> {
     unsafe {
-      let endpoint_host = NSString::new(&endpoint.host).to_cstr();
-      let endpoint_port = NSString::new(&endpoint.port).to_cstr();
-      let endpoint = nw_endpoint_create_host(endpoint_host, endpoint_port);
+      let endpoint_host =
+        CString::new(endpoint.host).map_err(|_| Error::ProxyEndpointCreationFailed)?;
+      let endpoint_port =
+        CString::new(endpoint.port).map_err(|_| Error::ProxyEndpointCreationFailed)?;
+      let endpoint = nw_endpoint_create_host(endpoint_host.as_ptr(), endpoint_port.as_ptr());
 
-      match endpoint {
-        #[allow(non_upper_case_globals)]
-        nil => Err(Error::ProxyEndpointCreationFailed),
-        _ => Ok(endpoint),
+      if endpoint.is_null() {
+        Err(Error::ProxyEndpointCreationFailed)
+      } else {
+        Ok(endpoint)
       }
     }
   }
diff --git a/src/wkwebview/synthetic_mouse_events.rs b/src/wkwebview/synthetic_mouse_events.rs
index d965090b9..97c427313 100644
--- a/src/wkwebview/synthetic_mouse_events.rs
+++ b/src/wkwebview/synthetic_mouse_events.rs
@@ -1,85 +1,76 @@
-use super::NSString;
-use cocoa::{
-  appkit::{NSEvent, NSEventModifierFlags, NSEventType, NSView},
-  base::{id, nil},
+use objc2_app_kit::{
+  NSAlternateKeyMask, NSCommandKeyMask, NSControlKeyMask, NSEvent, NSEventType, NSShiftKeyMask,
+  NSView,
 };
-use objc::{
-  declare::ClassDecl,
-  runtime::{Object, Sel},
-};
-use std::{ffi::c_void, ptr::null};
+use objc2_foundation::NSString;
 
-pub unsafe fn setup(decl: &mut ClassDecl) {
-  decl.add_method(
-    sel!(otherMouseDown:),
-    other_mouse_down as extern "C" fn(&mut Object, Sel, id),
-  );
-  decl.add_method(
-    sel!(otherMouseUp:),
-    other_mouse_up as extern "C" fn(&mut Object, Sel, id),
-  );
-}
+use super::WryWebView;
 
-extern "C" fn other_mouse_down(this: &mut Object, _sel: Sel, event: id) {
+pub(crate) fn other_mouse_down(this: &WryWebView, event: &NSEvent) {
   unsafe {
-    if event.eventType() == NSEventType::NSOtherMouseDown {
+    if event.r#type() == NSEventType::OtherMouseDown {
       let button_number = event.buttonNumber();
       match button_number {
         // back button
         3 => {
           let js = create_js_mouse_event(this, event, true, true);
-          let _: id = msg_send![this, evaluateJavaScript:NSString::new(&js) completionHandler:null::<*const c_void>()];
+          this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
           return;
         }
         // forward button
         4 => {
           let js = create_js_mouse_event(this, event, true, false);
-          let _: id = msg_send![this, evaluateJavaScript:NSString::new(&js) completionHandler:null::<*const c_void>()];
+          this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
           return;
         }
         _ => {}
       }
     }
 
-    let _: () = msg_send![this, mouseDown: event];
+    this.mouseDown(event);
   }
 }
-extern "C" fn other_mouse_up(this: &mut Object, _sel: Sel, event: id) {
+pub(crate) fn other_mouse_up(this: &WryWebView, event: &NSEvent) {
   unsafe {
-    if event.eventType() == NSEventType::NSOtherMouseUp {
+    if event.r#type() == NSEventType::OtherMouseUp {
       let button_number = event.buttonNumber();
       match button_number {
         // back button
         3 => {
           let js = create_js_mouse_event(this, event, false, true);
-          let _: id = msg_send![this, evaluateJavaScript:NSString::new(&js) completionHandler:null::<*const c_void>()];
+          this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
           return;
         }
         // forward button
         4 => {
           let js = create_js_mouse_event(this, event, false, false);
-          let _: id = msg_send![this, evaluateJavaScript:NSString::new(&js) completionHandler:null::<*const c_void>()];
+          this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None);
           return;
         }
         _ => {}
       }
     }
 
-    let _: () = msg_send![this, mouseUp: event];
+    this.mouseUp(event);
   }
 }
 
-unsafe fn create_js_mouse_event(view: id, event: id, down: bool, back_button: bool) -> String {
+unsafe fn create_js_mouse_event(
+  view: &NSView,
+  event: &NSEvent,
+  down: bool,
+  back_button: bool,
+) -> String {
   let event_name = if down { "mousedown" } else { "mouseup" };
   // js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
   let button = if back_button { 3 } else { 4 };
   let mods_flags = event.modifierFlags();
   let window_point = event.locationInWindow();
-  let view_point = view.convertPoint_fromView_(window_point, nil);
+  let view_point = view.convertPoint_fromView(window_point, None);
   let x = view_point.x as u32;
   let y = view_point.y as u32;
   // js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
-  let buttons = NSEvent::pressedMouseButtons(event);
+  let buttons = NSEvent::pressedMouseButtons();
 
   format!(
     r#"(() => {{
@@ -122,10 +113,10 @@ unsafe fn create_js_mouse_event(view: id, event: id, down: bool, back_button: bo
     x = x,
     y = y,
     detail = event.clickCount(),
-    ctrl_key = mods_flags.contains(NSEventModifierFlags::NSControlKeyMask),
-    alt_key = mods_flags.contains(NSEventModifierFlags::NSAlternateKeyMask),
-    shift_key = mods_flags.contains(NSEventModifierFlags::NSShiftKeyMask),
-    meta_key = mods_flags.contains(NSEventModifierFlags::NSCommandKeyMask),
+    ctrl_key = mods_flags.contains(NSControlKeyMask),
+    alt_key = mods_flags.contains(NSAlternateKeyMask),
+    shift_key = mods_flags.contains(NSShiftKeyMask),
+    meta_key = mods_flags.contains(NSCommandKeyMask),
     button = button,
     buttons = buttons,
   )
diff --git a/src/wkwebview/util.rs b/src/wkwebview/util.rs
index 31dfe029b..ca396ae53 100644
--- a/src/wkwebview/util.rs
+++ b/src/wkwebview/util.rs
@@ -2,16 +2,14 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use cocoa::{base::id, foundation::NSOperatingSystemVersion};
+use objc2_foundation::NSProcessInfo;
 
-pub fn operating_system_version() -> (u64, u64, u64) {
-  unsafe {
-    let process_info: id = msg_send![class!(NSProcessInfo), processInfo];
-    let version: NSOperatingSystemVersion = msg_send![process_info, operatingSystemVersion];
-    (
-      version.majorVersion,
-      version.minorVersion,
-      version.patchVersion,
-    )
-  }
+pub fn operating_system_version() -> (isize, isize, isize) {
+  let process_info = NSProcessInfo::processInfo();
+  let version = process_info.operatingSystemVersion();
+  (
+    version.majorVersion,
+    version.minorVersion,
+    version.patchVersion,
+  )
 }


Test patch: