wails/v3/pkg/application/application_ios.m
Lea Anthony 873848a077 Merge iOS support from v3-alpha-feature/ios-support
This commit integrates iOS platform support for Wails v3, adapting the
iOS-specific code to work with the new transport layer architecture.

Key changes:
- Add iOS-specific application, webview, and runtime files
- Add iOS event types and processing
- Add iOS examples and templates
- Update messageprocessor to handle iOS requests
- Move badge_ios.go to dock package

Note: The iOS branch was based on an older v3-alpha and required
significant conflict resolution due to the transport layer refactor
(PR #4702). Some iOS-specific code may need further adaptation:
- processIOSMethod needs to be implemented with new RuntimeRequest signature
- iOS event generation in tasks/events/generate.go needs updating

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 18:34:21 +11:00

324 lines
12 KiB
Objective-C

//go:build ios
#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>
#import "application_ios.h"
#import "application_ios_delegate.h"
#import "webview_window_ios.h"
#import <sys/utsname.h>
#import <stdlib.h>
#import <string.h>
// Forward declarations for Go callbacks
void ServeAssetRequest(unsigned int windowID, void* urlSchemeTask);
void HandleJSMessage(unsigned int windowID, char* message);
// Global references - declare after interface
WailsAppDelegate *appDelegate = nil;
unsigned int nextWindowID = 1;
static bool g_disableInputAccessory = false; // default: enabled (shown)
// New global flags with sensible iOS defaults
static bool g_disableScroll = false; // default: scrolling enabled
static bool g_disableBounce = false; // default: bounce enabled
static bool g_disableScrollIndicators = false; // default: indicators shown
static bool g_enableBackForwardGestures = false; // default: gestures disabled
static bool g_disableLinkPreview = false; // default: link preview enabled
static bool g_enableInlineMediaPlayback = false; // default: inline playback disabled
static bool g_enableAutoplayNoUserAction = false; // default: autoplay requires user action
static bool g_disableInspectable = false; // default: inspector enabled
static NSString* g_userAgent = nil;
static NSString* g_appNameForUA = nil; // default applied in code when nil
static bool g_enableNativeTabs = false; // default: off
static NSString* g_nativeTabsItemsJSON = nil; // JSON array of items
// App-wide background colour storage (RGBA 0-255)
static bool g_appBGSet = false;
static unsigned char g_appBG_R = 255;
static unsigned char g_appBG_G = 255;
static unsigned char g_appBG_B = 255;
static unsigned char g_appBG_A = 255;
// Note: The WailsAppDelegate implementation resides in application_ios_delegate.m
// C interface implementation
void ios_app_init(void) {
// This will be called from Go's init
// Explicitly reference WailsAppDelegate to ensure the class is linked and registered.
(void)[WailsAppDelegate class];
// The actual UI startup happens via UIApplicationMain in main.m
}
void ios_app_run(void) {
// This function is no longer used - UIApplicationMain is called from main.m
// The WailsAppDelegate is automatically instantiated by UIApplicationMain
extern void LogInfo(const char* source, const char* message);
LogInfo("ios_app_run", "⚠️ This function should not be called - UIApplicationMain runs from main.m");
}
void ios_app_quit(void) {
dispatch_async(dispatch_get_main_queue(), ^{
exit(0);
});
}
bool ios_is_dark_mode(void) {
if (@available(iOS 13.0, *)) {
UIUserInterfaceStyle style = [[UITraitCollection currentTraitCollection] userInterfaceStyle];
return style == UIUserInterfaceStyleDark;
}
return false;
}
void ios_set_disable_input_accessory(bool disabled) {
g_disableInputAccessory = disabled;
}
bool ios_is_input_accessory_disabled(void) {
return g_disableInputAccessory;
}
// Scrolling & bounce & indicators
void ios_set_disable_scroll(bool disabled) { g_disableScroll = disabled; }
bool ios_is_scroll_disabled(void) { return g_disableScroll; }
void ios_set_disable_bounce(bool disabled) { g_disableBounce = disabled; }
bool ios_is_bounce_disabled(void) { return g_disableBounce; }
void ios_set_disable_scroll_indicators(bool disabled) { g_disableScrollIndicators = disabled; }
bool ios_is_scroll_indicators_disabled(void) { return g_disableScrollIndicators; }
// Navigation gestures
void ios_set_enable_back_forward_gestures(bool enabled) { g_enableBackForwardGestures = enabled; }
bool ios_is_back_forward_gestures_enabled(void) { return g_enableBackForwardGestures; }
// Link previews
void ios_set_disable_link_preview(bool disabled) { g_disableLinkPreview = disabled; }
bool ios_is_link_preview_disabled(void) { return g_disableLinkPreview; }
// Media playback
void ios_set_enable_inline_media_playback(bool enabled) { g_enableInlineMediaPlayback = enabled; }
bool ios_is_inline_media_playback_enabled(void) { return g_enableInlineMediaPlayback; }
void ios_set_enable_autoplay_without_user_action(bool enabled) { g_enableAutoplayNoUserAction = enabled; }
bool ios_is_autoplay_without_user_action_enabled(void) { return g_enableAutoplayNoUserAction; }
// Inspector
void ios_set_disable_inspectable(bool disabled) { g_disableInspectable = disabled; }
bool ios_is_inspectable_disabled(void) { return g_disableInspectable; }
// User agent customization
void ios_set_user_agent(const char* ua) {
if (ua == NULL) { g_userAgent = nil; return; }
g_userAgent = [NSString stringWithUTF8String:ua];
}
const char* ios_get_user_agent(void) {
if (g_userAgent == nil) return NULL;
return [g_userAgent UTF8String];
}
void ios_set_app_name_for_user_agent(const char* name) {
if (name == NULL) { g_appNameForUA = nil; return; }
g_appNameForUA = [NSString stringWithUTF8String:name];
}
const char* ios_get_app_name_for_user_agent(void) {
if (g_appNameForUA == nil) return NULL;
return [g_appNameForUA UTF8String];
}
// Live runtime mutations (apply to existing WKWebView instances)
static void forEachViewController(void (^block)(WailsViewController *vc)) {
if (!appDelegate || !appDelegate.viewControllers) return;
void (^applyBlock)(void) = ^{
for (WailsViewController *vc in appDelegate.viewControllers) {
if (!vc || !vc.webView) continue;
block(vc);
}
};
if ([NSThread isMainThread]) {
applyBlock();
} else {
dispatch_async(dispatch_get_main_queue(), applyBlock);
}
}
void ios_runtime_set_scroll_enabled(bool enabled) {
g_disableScroll = !enabled;
forEachViewController(^(WailsViewController *vc){
vc.webView.scrollView.scrollEnabled = enabled ? YES : NO;
});
}
void ios_runtime_set_bounce_enabled(bool enabled) {
g_disableBounce = !enabled;
forEachViewController(^(WailsViewController *vc){
UIScrollView *sv = vc.webView.scrollView;
sv.bounces = enabled ? YES : NO;
sv.alwaysBounceVertical = enabled ? YES : NO;
sv.alwaysBounceHorizontal = enabled ? YES : NO;
});
}
void ios_runtime_set_scroll_indicators_enabled(bool enabled) {
g_disableScrollIndicators = !enabled;
forEachViewController(^(WailsViewController *vc){
UIScrollView *sv = vc.webView.scrollView;
sv.showsVerticalScrollIndicator = enabled ? YES : NO;
sv.showsHorizontalScrollIndicator = enabled ? YES : NO;
});
}
void ios_runtime_set_back_forward_gestures_enabled(bool enabled) {
g_enableBackForwardGestures = enabled;
forEachViewController(^(WailsViewController *vc){
vc.webView.allowsBackForwardNavigationGestures = enabled ? YES : NO;
});
}
void ios_runtime_set_link_preview_enabled(bool enabled) {
g_disableLinkPreview = !enabled;
forEachViewController(^(WailsViewController *vc){
vc.webView.allowsLinkPreview = enabled ? YES : NO;
});
}
void ios_runtime_set_inspectable_enabled(bool enabled) {
g_disableInspectable = !enabled;
forEachViewController(^(WailsViewController *vc){
BOOL inspectorOn = enabled ? YES : NO;
if (@available(iOS 16.4, *)) {
vc.webView.inspectable = inspectorOn;
} else {
@try { [vc.webView setValue:@(inspectorOn) forKey:@"inspectable"]; } @catch (__unused NSException *e) {}
}
});
}
void ios_runtime_set_custom_user_agent(const char* ua) {
ios_set_user_agent(ua);
NSString *uaStr = (ua ? [NSString stringWithUTF8String:ua] : nil);
forEachViewController(^(WailsViewController *vc){
vc.webView.customUserAgent = uaStr;
});
}
// Forward declaration used by getters
static const char* dupCString(NSString *str);
// Native bottom tab bar controls
void ios_native_tabs_set_enabled(bool enabled) {
g_enableNativeTabs = enabled;
NSLog(@"[ios_native_tabs_set_enabled] enabled=%d", enabled);
__block NSUInteger count = 0;
forEachViewController(^(WailsViewController *vc){
count++;
[vc enableNativeTabs:(enabled ? YES : NO)];
});
NSLog(@"[ios_native_tabs_set_enabled] applied to %lu view controller(s)", (unsigned long)count);
}
bool ios_native_tabs_is_enabled(void) {
return g_enableNativeTabs;
}
void ios_native_tabs_select_index(int index) {
forEachViewController(^(WailsViewController *vc){
[vc selectNativeTabIndex:(NSInteger)index];
});
}
void ios_native_tabs_set_items_json(const char* json) {
if (json == NULL) {
g_nativeTabsItemsJSON = nil;
NSLog(@"[ios_native_tabs_set_items_json] JSON cleared (nil)");
return;
}
g_nativeTabsItemsJSON = [NSString stringWithUTF8String:json];
NSLog(@"[ios_native_tabs_set_items_json] JSON length=%lu", (unsigned long)g_nativeTabsItemsJSON.length);
// Apply to existing controllers if visible
__block NSUInteger refreshed = 0;
forEachViewController(^(WailsViewController *vc){
if (vc.tabBar && !vc.tabBar.isHidden) {
// Re-enable to rebuild items from JSON
[vc enableNativeTabs:YES];
refreshed++;
}
});
NSLog(@"[ios_native_tabs_set_items_json] refreshed %lu visible tab bar(s)", (unsigned long)refreshed);
}
const char* ios_native_tabs_get_items_json(void) {
return dupCString(g_nativeTabsItemsJSON);
}
// App-wide background colour control
void ios_set_app_background_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a, bool isSet) {
g_appBGSet = isSet;
if (isSet) {
g_appBG_R = r; g_appBG_G = g; g_appBG_B = b; g_appBG_A = a;
}
}
bool ios_get_app_background_color(unsigned char* r, unsigned char* g, unsigned char* b, unsigned char* a) {
if (!g_appBGSet) return false;
if (r) *r = g_appBG_R;
if (g) *g = g_appBG_G;
if (b) *b = g_appBG_B;
if (a) *a = g_appBG_A;
return true;
}
// iOS Runtime bridges
void ios_haptics_impact(const char* cstyle) {
if (cstyle == NULL) return;
NSString *style = [NSString stringWithUTF8String:cstyle];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"[ios_haptics_impact] requested style=%@", style);
if (@available(iOS 13.0, *)) {
UIImpactFeedbackStyle feedbackStyle = UIImpactFeedbackStyleMedium;
if ([style isEqualToString:@"light"]) feedbackStyle = UIImpactFeedbackStyleLight;
else if ([style isEqualToString:@"medium"]) feedbackStyle = UIImpactFeedbackStyleMedium;
else if ([style isEqualToString:@"heavy"]) feedbackStyle = UIImpactFeedbackStyleHeavy;
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
else if ([style isEqualToString:@"soft"]) feedbackStyle = UIImpactFeedbackStyleSoft;
else if ([style isEqualToString:@"rigid"]) feedbackStyle = UIImpactFeedbackStyleRigid;
#endif
UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:feedbackStyle];
[generator prepare];
[generator impactOccurred];
#if TARGET_OS_SIMULATOR
NSLog(@"[ios_haptics_impact] Simulator detected: no physical haptic feedback will be felt.");
#else
NSLog(@"[ios_haptics_impact] Haptic impact triggered.");
#endif
} else {
NSLog(@"[ios_haptics_impact] iOS version < 13.0: no haptic API available.");
}
});
}
static const char* dupCString(NSString *str) {
if (str == nil) return NULL;
const char* utf8 = [str UTF8String];
if (utf8 == NULL) return NULL;
size_t len = strlen(utf8) + 1;
char* out = (char*)malloc(len);
if (out) memcpy(out, utf8, len);
return out;
}
const char* ios_device_info_json(void) {
UIDevice *device = [UIDevice currentDevice];
struct utsname systemInfo;
uname(&systemInfo);
NSString *model = [NSString stringWithUTF8String:systemInfo.machine];
NSString *systemName = device.systemName ?: @"iOS";
NSString *systemVersion = device.systemVersion ?: @"";
#if TARGET_OS_SIMULATOR
BOOL isSimulator = YES;
#else
BOOL isSimulator = NO;
#endif
NSString *json = [NSString stringWithFormat:
@"{\"model\":\"%@\",\"systemName\":\"%@\",\"systemVersion\":\"%@\",\"isSimulator\":%@}",
model, systemName, systemVersion, isSimulator ? @"true" : @"false"
];
return dupCString(json);
}