Skip to content

Commit e9e4af4

Browse files
committed
progress
1 parent 56196ac commit e9e4af4

File tree

11 files changed

+745
-2
lines changed

11 files changed

+745
-2
lines changed

Demo/InertiaDemo/InertiaDemo.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import UIKit
2+
import HotwireNative
3+
4+
@main
5+
class AppDelegate: UIResponder, UIApplicationDelegate {
6+
var window: UIWindow?
7+
8+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
9+
// Configure Inertia
10+
Inertia.config.showDoneButtonOnModals = true
11+
Inertia.config.debugLoggingEnabled = true
12+
13+
// Create window
14+
window = UIWindow(frame: UIScreen.main.bounds)
15+
16+
// Set up the root view controller
17+
let url = URL(string: "https://your-inertia-app.com")!
18+
let navigationController = Inertia.navigate(to: url)
19+
20+
window?.rootViewController = navigationController
21+
window?.makeKeyAndVisible()
22+
23+
return true
24+
}
25+
}
26+
27+
// Custom Inertia web view controller
28+
class CustomInertiaViewController: InertiaWebViewController {
29+
override func viewDidLoad() {
30+
super.viewDidLoad()
31+
32+
// Add a refresh control
33+
let refreshControl = UIRefreshControl()
34+
refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
35+
session.webView.scrollView.refreshControl = refreshControl
36+
}
37+
38+
@objc func refresh() {
39+
session.reload()
40+
session.webView.scrollView.refreshControl?.endRefreshing()
41+
}
42+
43+
// Handle Inertia.js events
44+
override func inertiaSession(_ session: InertiaSession, didNavigateToPage url: String, component: String, props: [String: Any]?) {
45+
super.inertiaSession(session, didNavigateToPage: url, component: component, props: props)
46+
47+
// Log navigation
48+
print("Navigated to \(component) at \(url)")
49+
if let props = props {
50+
print("Props: \(props)")
51+
}
52+
}
53+
}

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ let package = Package(
2424
path: "Source",
2525
resources: [
2626
.copy("Turbo/WebView/turbo.js"),
27-
.copy("Bridge/bridge.js")
27+
.copy("Bridge/bridge.js"),
28+
.copy("Inertia/inertia-bridge.js")
2829
]
2930
),
3031
.testTarget(

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
![Swift](https://img.shields.io/badge/Swift-5.3-blue)
44
![iOS](https://img.shields.io/badge/iOS-14+-green)
55
![Turbo](https://img.shields.io/badge/Turbo-7+-purple)
6+
![Inertia](https://img.shields.io/badge/Inertia-1.0+-orange)
67

78
[Hotwire Native](https://native.hotwired.dev) is a high-level native framework, available for iOS and Android, that provides you with all the tools you need to leverage your web app and build great mobile apps.
89

910
This native Swift library integrates with your [Hotwire](https://hotwired.dev) web app by wrapping it in a native iOS shell. It manages a single WKWebView instance across multiple view controllers, giving you native navigation UI with all the client-side performance benefits of Hotwire.
1011

12+
## Inertia.js Support
13+
14+
This fork adds support for [Inertia.js](https://inertiajs.com/) applications, allowing you to build single-page applications using server-side frameworks like Laravel, Rails, or Django with native iOS navigation and components. See the [Inertia.js integration documentation](Source/Inertia/README.md) for more details.
15+
1116
Read more on [native.hotwired.dev](https://native.hotwired.dev).
1217

1318
## Contributing
@@ -16,6 +21,6 @@ Hotwire Native for iOS is open-source software, freely distributable under the t
1621

1722
We welcome contributions in the form of bug reports, pull requests, or thoughtful discussions in the [GitHub issue tracker](https://github.com/hotwired/hotwire-native-bridge/issues).
1823

19-
---------
24+
---
2025

2126
© 2024 37signals LLC

Source/Inertia/Inertia.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import WebKit
2+
3+
/// Main entry point for Inertia.js integration
4+
public enum Inertia {
5+
/// Use this instance to configure Inertia.js integration
6+
public static var config = InertiaConfig()
7+
8+
/// Registers your bridge components to use with `InertiaWebViewController`.
9+
///
10+
/// Use `Inertia.config.makeCustomWebView` to customize the web view or web view
11+
/// configuration further.
12+
public static func registerBridgeComponents(_ componentTypes: [BridgeComponent.Type]) {
13+
bridgeComponentTypes = componentTypes
14+
}
15+
16+
/// Creates a new Inertia navigation controller with the given URL
17+
/// - Parameter url: The URL to load
18+
/// - Returns: A new InertiaNavigationController
19+
public static func navigate(to url: URL) -> InertiaNavigationController {
20+
return InertiaNavigationController(url: url)
21+
}
22+
23+
/// Injects the Inertia.js bridge script into a web view configuration
24+
/// - Parameter configuration: The web view configuration to modify
25+
/// - Returns: The modified configuration
26+
public static func injectBridge(into configuration: WKWebViewConfiguration) -> WKWebViewConfiguration {
27+
guard let bridgeURL = Bundle.module.url(forResource: "inertia-bridge", withExtension: "js"),
28+
let bridgeScript = try? String(contentsOf: bridgeURL) else {
29+
return configuration
30+
}
31+
32+
let userScript = WKUserScript(source: bridgeScript, injectionTime: .atDocumentStart, forMainFrameOnly: true)
33+
configuration.userContentController.addUserScript(userScript)
34+
return configuration
35+
}
36+
37+
/// Bridge component types registered with the Inertia.js integration
38+
static var bridgeComponentTypes = [BridgeComponent.Type]()
39+
}

Source/Inertia/InertiaConfig.swift

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import UIKit
2+
import WebKit
3+
4+
/// Configuration for Inertia.js integration
5+
public struct InertiaConfig {
6+
public typealias WebViewBlock = (_ configuration: WKWebViewConfiguration) -> WKWebView
7+
8+
/// Set a custom user agent application prefix for every WKWebView instance.
9+
///
10+
/// The library will automatically append a substring to your prefix
11+
/// which includes:
12+
/// - "Inertia Native iOS;"
13+
/// - "bridge-components: [your bridge components];"
14+
///
15+
/// WKWebView's default user agent string will also appear at the
16+
/// beginning of the user agent.
17+
public var applicationUserAgentPrefix: String? = nil
18+
19+
/// When enabled, adds a `UIBarButtonItem` of type `.done` to the left
20+
/// navigation bar button item on screens presented modally.
21+
public var showDoneButtonOnModals = false
22+
23+
/// Sets the back button display mode of `InertiaWebViewController`.
24+
public var backButtonDisplayMode = UINavigationItem.BackButtonDisplayMode.default
25+
26+
/// Enable or disable debug logging for Inertia visits and bridge elements
27+
/// connecting, disconnecting, receiving/sending messages, and more.
28+
public var debugLoggingEnabled = false {
29+
didSet {
30+
HotwireLogger.debugLoggingEnabled = debugLoggingEnabled
31+
}
32+
}
33+
34+
/// Gets the user agent that the library builds to identify the app
35+
/// and its registered bridge components.
36+
///
37+
/// The user agent includes:
38+
/// - Your (optional) custom `applicationUserAgentPrefix`
39+
/// - "Native iOS; Inertia Native iOS;"
40+
/// - "bridge-components: [your bridge components];"
41+
public var userAgent: String {
42+
get {
43+
return UserAgent.build(
44+
applicationPrefix: applicationUserAgentPrefix,
45+
componentTypes: Inertia.bridgeComponentTypes
46+
)
47+
}
48+
}
49+
50+
/// Optionally customize the web views used by each Inertia Session.
51+
/// Ensure you return a new instance each time.
52+
public var makeCustomWebView: WebViewBlock = { (configuration: WKWebViewConfiguration) in
53+
WKWebView.debugInspectable(configuration: configuration)
54+
}
55+
56+
/// Set a custom JSON encoder when parsing bridge payloads.
57+
/// The custom encoder can be useful when you need to apply specific
58+
/// encoding strategies, like snake case vs. camel case
59+
public var jsonEncoder = JSONEncoder()
60+
61+
/// Set a custom JSON decoder when parsing bridge payloads.
62+
/// The custom decoder can be useful when you need to apply specific
63+
/// decoding strategies, like snake case vs. camel case
64+
public var jsonDecoder = JSONDecoder()
65+
66+
// MARK: - Internal
67+
68+
public func makeWebView() -> WKWebView {
69+
let configuration = makeWebViewConfiguration()
70+
let webView = makeCustomWebView(configuration)
71+
72+
if !Inertia.bridgeComponentTypes.isEmpty {
73+
Bridge.initialize(webView)
74+
}
75+
76+
return webView
77+
}
78+
79+
// MARK: - Private
80+
81+
private let sharedProcessPool = WKProcessPool()
82+
83+
// A method (not a property) because we need a new instance for each web view.
84+
private func makeWebViewConfiguration() -> WKWebViewConfiguration {
85+
let configuration = WKWebViewConfiguration()
86+
configuration.defaultWebpagePreferences?.preferredContentMode = .mobile
87+
configuration.applicationNameForUserAgent = userAgent
88+
configuration.processPool = sharedProcessPool
89+
90+
// Inject the Inertia bridge script
91+
return Inertia.injectBridge(into: configuration)
92+
}
93+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import UIKit
2+
3+
/// A navigation controller for Inertia.js web views
4+
open class InertiaNavigationController: UINavigationController {
5+
/// The shared session for all view controllers in this navigation stack
6+
public let session: InertiaSession
7+
8+
/// Initialize with a root view controller and optional session
9+
/// - Parameters:
10+
/// - rootViewController: The root view controller
11+
/// - session: An optional InertiaSession. If nil, a new session will be created.
12+
public init(rootViewController: UIViewController, session: InertiaSession? = nil) {
13+
self.session = session ?? InertiaSession(webView: Inertia.config.makeWebView())
14+
super.init(rootViewController: rootViewController)
15+
}
16+
17+
/// Initialize with a URL to load
18+
/// - Parameters:
19+
/// - url: The URL to load
20+
/// - session: An optional InertiaSession. If nil, a new session will be created.
21+
public convenience init(url: URL, session: InertiaSession? = nil) {
22+
let sharedSession = session ?? InertiaSession(webView: Inertia.config.makeWebView())
23+
let rootViewController = InertiaWebViewController(url: url, session: sharedSession)
24+
self.init(rootViewController: rootViewController, session: sharedSession)
25+
}
26+
27+
required public init?(coder aDecoder: NSCoder) {
28+
fatalError("init(coder:) has not been implemented")
29+
}
30+
31+
/// Push a new Inertia web view controller onto the navigation stack
32+
/// - Parameter url: The URL to load
33+
/// - Returns: The pushed view controller
34+
@discardableResult
35+
public func push(url: URL) -> InertiaWebViewController {
36+
let viewController = InertiaWebViewController(url: url, session: session)
37+
pushViewController(viewController, animated: true)
38+
return viewController
39+
}
40+
41+
/// Present a new Inertia web view controller modally
42+
/// - Parameters:
43+
/// - url: The URL to load
44+
/// - animated: Whether to animate the presentation
45+
/// - completion: A completion handler
46+
/// - Returns: The presented navigation controller
47+
@discardableResult
48+
public func present(url: URL, animated: Bool = true, completion: (() -> Void)? = nil) -> InertiaNavigationController {
49+
// Create a new session for the modal
50+
let modalSession = InertiaSession(webView: Inertia.config.makeWebView())
51+
let navigationController = InertiaNavigationController(url: url, session: modalSession)
52+
present(navigationController, animated: animated, completion: completion)
53+
return navigationController
54+
}
55+
}

0 commit comments

Comments
 (0)