Documentation Index
Fetch the complete documentation index at: https://mintlify.com/frol/near-connect-ios/llms.txt
Use this file to discover all available pages before exploring further.
NEAR Connect iOS is built with a clean separation of concerns, following SwiftUI best practices while maintaining a persistent WebView bridge for wallet communication.
Component Overview
NEARConnect Package
├── NEARWalletManager # Core manager (@MainActor, ObservableObject)
│ ├── bridgeWebView # Persistent WKWebView instance
│ ├── coordinator # WebViewCoordinator (delegates)
│ ├── Published state # currentAccount, isBusy, showWalletUI
│ └── Continuations # Async/await bridges
│
├── WebViewCoordinator # WKWebView delegate handler
│ ├── WKScriptMessageHandler # JS → Swift messages
│ ├── WKNavigationDelegate # URL handling
│ ├── WKUIDelegate # Popup windows
│ └── popupWebViews[] # Managed popup windows
│
├── BridgeWebViewContainer # UIViewRepresentable wrapper
│ └── Wraps WKWebView for SwiftUI
│
├── WalletBridgeSheet # Ready-to-use UI component
│ ├── Shows BridgeWebViewContainer
│ ├── Loading overlay
│ └── Close button
│
├── NEARAccount # Account model (Codable, Sendable)
├── NEARError # Error types
├── NEARConnectEvent # Internal bridge events
└── Resources/
└── near-connect-bridge.html # HTML + JavaScript bridge
Core Components
NEARWalletManager
The NEARWalletManager is the central coordinator marked with @MainActor to ensure all UI updates happen on the main thread.
Published Properties
@MainActor
public class NEARWalletManager: ObservableObject {
// State that triggers UI updates
@Published public var currentAccount: NEARAccount?
@Published public var isBusy = false
@Published public var lastError: String?
@Published public private(set) var isBridgeReady = false
@Published public var showWalletUI = false
// Network configuration
@Published public var network: Network = .mainnet
// Computed properties
public var isSignedIn: Bool { currentAccount != nil }
}
The @Published properties automatically trigger SwiftUI view updates when changed, providing reactive UI behavior without manual state management.
WebView Ownership
The manager owns the persistent WebView and its coordinator:
public private(set) var bridgeWebView: WKWebView!
private var coordinator: WebViewCoordinator!
This ownership model ensures:
- The WebView lives as long as the manager
- JavaScript state persists across UI presentations
- Wallet sessions survive view hierarchy changes
- Memory is properly managed when the manager is deallocated
Continuation Management
Async operations use checked continuations stored as optional properties:
private var signInContinuation: CheckedContinuation<NEARAccount, any Error>?
private var transactionContinuation: CheckedContinuation<TransactionResult, any Error>?
private var messageContinuation: CheckedContinuation<MessageSignResult, any Error>?
private var delegateActionContinuation: CheckedContinuation<DelegateActionResult, any Error>?
private var signInAndSignMessageContinuation: CheckedContinuation<SignInWithMessageResult, any Error>?
Each continuation represents a pending async operation. When a JavaScript event arrives, the corresponding continuation is resumed and set to nil.
Only one operation of each type can be in flight at a time. The isBusy flag provides additional protection against concurrent operations.
Persistence Layer
private let userDefaults: UserDefaults
private let accountStorageKey = "near_connected_account"
private func saveAccount(_ account: NEARAccount) {
if let data = try? JSONEncoder().encode(account) {
userDefaults.set(data, forKey: accountStorageKey)
}
}
private func loadStoredAccount() {
guard let data = userDefaults.data(forKey: accountStorageKey),
let account = try? JSONDecoder().decode(NEARAccount.self, from: data) else {
return
}
currentAccount = account
}
The account is loaded during initialization and saved whenever the user connects a wallet.
WebViewCoordinator
The WebViewCoordinator implements all WKWebView delegate protocols, acting as the bridge between WebKit and the Swift manager.
Responsibilities
1. Script Message Handling (WKScriptMessageHandler)
Receives messages from JavaScript and converts them to typed Swift events:
func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
guard message.name == "nearConnect",
let body = message.body as? [String: Any],
let type = body["type"] as? String else {
return
}
let event: NEARConnectEvent
switch type {
case "ready":
event = .ready
case "signIn":
event = .signedIn(...)
case "transactionResult":
event = .transactionResult(...)
// ... handle all event types
}
self.manager?.handleEvent(event)
}
2. Navigation Handling (WKNavigationDelegate)
Intercepts navigation requests to handle deep links:
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
let isPopup = popupWebViews.contains(where: { $0 === webView })
if isPopup, shouldOpenExternally(url) {
decisionHandler(.cancel)
UIApplication.shared.open(url) // Open in system/native app
webView.removeFromSuperview()
popupWebViews.removeAll { $0 === webView }
return
}
decisionHandler(.allow)
}
3. Popup Window Management (WKUIDelegate)
Creates and manages popup WebViews for wallet authentication:
func webView(
_ webView: WKWebView,
createWebViewWith configuration: WKWebViewConfiguration,
for navigationAction: WKNavigationAction,
windowFeatures: WKWindowFeatures
) -> WKWebView? {
let url = navigationAction.request.url
if let url, shouldOpenExternally(url) {
UIApplication.shared.open(url)
return nil
}
// Enable cookies for Cloudflare-protected wallets
configuration.websiteDataStore = .default()
let popup = WKWebView(frame: webView.bounds, configuration: configuration)
popup.navigationDelegate = self
popup.uiDelegate = self
manager.bridgeWebView.addSubview(popup)
popupWebViews.append(popup)
return popup
}
Popup windows are added as subviews of the bridge WebView, not directly to the view hierarchy. This ensures they move with the bridge when it’s presented/dismissed.
Popups are tracked and cleaned up automatically:
var popupWebViews: [WKWebView] = []
func closeAllPopups() {
for popup in popupWebViews {
popup.removeFromSuperview()
}
popupWebViews.removeAll()
}
func webViewDidClose(_ webView: WKWebView) {
webView.removeFromSuperview()
popupWebViews.removeAll { $0 === webView }
}
BridgeWebViewContainer
A UIViewRepresentable that wraps the persistent WebView for use in SwiftUI:
public struct BridgeWebViewContainer: UIViewRepresentable {
public let webView: WKWebView
public func makeUIView(context: Context) -> UIView {
let container = UIView()
container.backgroundColor = .systemBackground
webView.frame = container.bounds
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
container.addSubview(webView)
return container
}
public func updateUIView(_ uiView: UIView, context: Context) {
if webView.superview !== uiView {
webView.frame = uiView.bounds
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
uiView.addSubview(webView)
}
}
}
The updateUIView check ensures the WebView is only moved when necessary, preventing unnecessary view hierarchy changes.
WalletBridgeSheet
A ready-to-use full-screen sheet that combines the bridge WebView with a loading overlay and close button:
public struct WalletBridgeSheet: View {
@EnvironmentObject var walletManager: NEARWalletManager
@State private var didTrigger = false
public var body: some View {
ZStack {
// Bridge WebView (always present)
BridgeWebViewContainer(webView: walletManager.bridgeWebView)
.ignoresSafeArea()
// Loading overlay (shown while bridge initializes)
if !walletManager.isBridgeReady {
VStack(spacing: 16) {
ProgressView()
Text("Loading wallet connector...")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(uiColor: .systemBackground))
.ignoresSafeArea()
}
// Close button (always visible)
VStack {
HStack {
Spacer()
Button(action: cancelFlow) {
Image(systemName: "xmark.circle.fill")
.font(.title)
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .gray.opacity(0.6))
}
.padding(.top, 8)
.padding(.trailing, 16)
}
Spacer()
}
}
.onChange(of: walletManager.isBridgeReady) { _ in
triggerConnectIfNeeded()
}
.onAppear {
triggerConnectIfNeeded()
}
.onDisappear {
walletManager.cleanUpOnDismiss()
}
}
}
Automatic Flow Triggering
The sheet automatically triggers pending wallet operations when it appears and the bridge is ready:
private func triggerConnectIfNeeded() {
guard walletManager.isBridgeReady, !didTrigger else { return }
if let params = walletManager.pendingSignMessageParams {
// Connect with message signing
didTrigger = true
walletManager.pendingSignMessageParams = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
walletManager.triggerConnectWithSignMessage(
message: params.message,
recipient: params.recipient,
nonce: params.nonce
)
}
} else if walletManager.pendingConnect {
// Standard connect
didTrigger = true
walletManager.pendingConnect = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
walletManager.triggerWalletSelector()
}
}
}
The 0.3 second delay ensures the WebView has fully rendered before triggering JavaScript, preventing timing issues with the near-connect library initialization.
WebView Lifecycle
Initialization Phase
Presentation Phase
Transaction Phase
Cleanup Phase
Coordination Pattern
The architecture uses a coordinator pattern where the NEARWalletManager orchestrates communication between:
1. SwiftUI Views
- Observe
@Published properties
- Bind
showWalletUI to sheet presentation
- Call async methods like
connect(), sendNEAR(), etc.
2. WebView Layer
- Execute JavaScript functions via
evaluateJavaScript
- Receive events via
WKScriptMessageHandler
- Manage popup windows via
WKUIDelegate
3. Persistence Layer
- Store account to UserDefaults on sign-in
- Load account from UserDefaults on initialization
- Clear account on disconnect
4. Network Layer
- RPC calls for account queries
- Network selection affects bridge initialization
- Separate RPC endpoints for mainnet/testnet
Event Flow Example: Sending NEAR
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ SwiftUI │ │ NEARWalletManager│ │ WebView │
└──────┬──────┘ └────────┬─────────┘ └──────┬──────┘
│ │ │
│ sendNEAR(to:amount:) │ │
│────────────────────────>│ │
│ │ │
│ │ Store continuation │
│ │ showWalletUI = true │
│ │ │
│<────────────────────────│ │
│ Sheet presents │ │
│ │ │
│ │ evaluateJavaScript │
│ │ (nearSignAndSendTx) │
│ │─────────────────────────>│
│ │ │
│ │ │ JS shows
│ │ │ approval UI
│ │ │
│ │ │ User approves
│ │ │
│ │ postMessage(txResult) │
│ │<─────────────────────────│
│ │ │
│ │ Resume continuation │
│ │ showWalletUI = false │
│ │ │
│ Sheet dismisses │ │
│<────────────────────────│ │
│ │ │
│ Return TransactionResult│ │
│<────────────────────────│ │
│ │ │
Thread Safety
@MainActor Isolation
The NEARWalletManager is marked with @MainActor to ensure all property access and method calls happen on the main thread:
@MainActor
public class NEARWalletManager: ObservableObject {
// All methods and properties are main-actor-isolated
}
WebKit Callbacks
WebKit delegate methods are nonisolated by default but use MainActor.assumeIsolated to safely update manager state:
nonisolated func userContentController(
_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage
) {
MainActor.assumeIsolated {
// Safe to access @MainActor properties here
self.manager?.handleEvent(event)
}
}
This pattern ensures all UI updates happen on the main thread without requiring explicit DispatchQueue.main.async calls throughout the codebase.
Memory Management
The architecture uses Swift’s automatic reference counting (ARC) with careful attention to reference cycles:
WebViewCoordinator holds a weak reference to the manager
- The manager holds strong references to the WebView and coordinator
- Popup WebViews are removed from the hierarchy and deallocated when closed
- The manager is typically owned by the app’s root view via
@StateObject
class WebViewCoordinator: NSObject {
private weak var manager: NEARWalletManager? // Weak to prevent cycle
init(manager: NEARWalletManager) {
self.manager = manager
}
}