LangWars (iOS Native) (C# / Mobile / iOS-Native)
Note
This demo is available in your FlexCel installation at <FlexCel Install Folder>\samples\csharp\VS2026\Mobile\iOS-Native\LangWars and also at https://github.com/tmssoftware/TMS-FlexCel.NET-demos/tree/master/csharp/VS2026/Mobile/iOS-Native/LangWars
Overview
This example shows how to generate a report with FlexCel on iOS. It fetches the most popular programming language tags from the Stack Overflow API, fills a pre-designed Excel template with the data, and exports the result to HTML for display in a WKWebView. You can switch between online and offline data in case you don't have internet access.
How It Works
- Data fetching: The app calls the Stack Exchange API to retrieve the most-used tags, or loads a bundled offline JSON file (
OfflineData.txt) from the app bundle. - Report generation: A
FlexCelReportloads the template file (report.template.xlsx) from the bundle, binds the data table, and runs the report to produce a filled workbook with charts. - HTML export: The generated workbook is exported to HTML using
FlexCelHtmlExportwith embedded SVG images for crisp vector charts. - Display: The HTML file is loaded into a
WKWebViewviaLoadFileUrl(). - Sharing: A Share button lets you send the generated
.xlsxfile to other apps viaUIDocumentInteractionController.
FlexCel Features Demonstrated
- FlexCelReport: Template-based report generation with
AddTable()for data binding - FlexCelHtmlExport: Excel-to-HTML conversion with SVG images, HTML5 support, and embedded images
- XlsFile: Loading and saving Excel workbooks
Implementation Details
- Renderer.cs: Core logic for fetching data, running the report, and generating HTML. Uses
NSBundle.MainBundle.BundlePathfor accessing bundled resources. Generated files are stored inInternetCache(appropriate for re-creatable content on iOS). - FileSender.cs: Shares the generated Excel file using
UIDocumentInteractionController, presenting the share sheet from a toolbar button. - SceneDelegate.cs: Builds the UI programmatically with a toolbar (Fight button, Online/Offline switch, Share button) and an activity spinner during report generation. Uses
InvokeOnMainThreadfor thread-safe UI updates.
Files
AppDelegate.cs
namespace LangWars
{
[Register("AppDelegate")]
public class AppDelegate : UIApplicationDelegate
{
public override bool FinishedLaunching(UIApplication application, NSDictionary? launchOptions)
{
// Override point for customization after application launch.
return true;
}
public override UISceneConfiguration GetConfiguration(UIApplication application, UISceneSession connectingSceneSession, UISceneConnectionOptions options)
{
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
// "Default Configuration" is defined in the Info.plist's 'UISceneConfigurationName' key.
return new UISceneConfiguration("Default Configuration", connectingSceneSession.Role);
}
public override void DidDiscardSceneSessions(UIApplication application, NSSet<UISceneSession> sceneSessions)
{
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after 'FinishedLaunching'.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
}
FileSender.cs
namespace LangWars
{
public static class FileSender
{
public static void SendFile(string fileName, UIBarButtonItem sender)
{
var xlsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.InternetCache),
"langwars.xlsx");
if (!File.Exists(xlsPath))
return;
UIDocumentInteractionController docController = new()
{
Url = NSUrl.FromFilename(fileName)
};
docController.PresentOptionsMenu(sender, true);
}
}
}
LangData.cs
using System;
using System.Drawing;
using System.Runtime.Serialization;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace LangWars;
public sealed record class LangDataList(
[property: JsonPropertyName("items")] IReadOnlyList<LangData> Items);
public sealed record class LangData(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("count")] int Count);
[JsonSerializable(typeof(LangDataList))]
internal partial class LangDataListContext : JsonSerializerContext
{
}
Main.cs
using LangWars;
// This is the main entry point of the application.
// If you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
Renderer.cs
using FlexCel.Core;
using FlexCel.Render;
using FlexCel.Report;
using FlexCel.XlsAdapter;
using System.Text.Json;
namespace LangWars
{
public static class Renderer
{
public static string TempXlsPath
{
get
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.InternetCache),
"langwars.xlsx");
}
}
static string TempHtmlPath
{
get
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.InternetCache),
"langwars.html");
}
}
public static async Task<string> Display(bool offline)
{
var Langs = offline ? await LoadData() : await FetchData();
Langs = new LangDataList(Langs.Items.OrderByDescending(l => l.Count).ToList());
var xls = RunReport(Langs);
xls.Save(TempXlsPath); //we save it to share it and to display it on startup.
GenerateHTML(xls);
return TempHtmlPath;
}
static async Task<LangDataList> FetchData()
{
using var httpClient = new HttpClient();
var responseMessage = await httpClient.GetAsync("https://api.stackexchange.com/2.3/tags?order=desc&sort=popular&site=stackoverflow&pagesize=5");
responseMessage.EnsureSuccessStatusCode();
using var stream = await responseMessage.Content.ReadAsStreamAsync();
var result = await JsonSerializer.DeserializeAsync(stream, LangDataListContext.Default.LangDataList);
return result ?? throw new InvalidOperationException("Failed to deserialize language data from API response.");
}
static async Task<LangDataList> LoadData()
{
using var offlineData = new FileStream(Path.Combine(NSBundle.MainBundle.BundlePath, "OfflineData.txt"), FileMode.Open, FileAccess.Read);
var result = await JsonSerializer.DeserializeAsync(offlineData, LangDataListContext.Default.LangDataList);
return result ?? throw new InvalidOperationException("Failed to deserialize offline language data.");
}
static ExcelFile RunReport(LangDataList langs)
{
ExcelFile Result = new XlsFile(Path.Combine(NSBundle.MainBundle.BundlePath, "report.template.xlsx"), true);
using (FlexCelReport fr = new(true))
{
fr.AddTable("lang", langs.Items);
fr.Run(Result);
}
return Result;
}
static void GenerateHTML(ExcelFile xls)
{
using FlexCelHtmlExport html = new(xls, true);
//If we were using png, we would have to set
//a high resolution so this looks nice in high resolution displays.
//html.ImageResolution = 326;
//but we will use SVG, which is vectorial:
html.HtmlVersion = THtmlVersion.Html_5;
html.SavedImagesFormat = THtmlImageFormat.Svg;
html.EmbedImages = true;
html.Export(TempHtmlPath, ".");
// Center the content horizontally in the webview
var content = File.ReadAllText(TempHtmlPath);
content = content.Replace("<body", "<body style=\"display:flex;justify-content:center\"");
File.WriteAllText(TempHtmlPath, content);
}
}
}
SceneDelegate.cs
using WebKit;
namespace LangWars
{
[Register("SceneDelegate")]
public class SceneDelegate : UIResponder, IUIWindowSceneDelegate
{
WKWebView? webView;
[Export("window")]
public UIWindow? Window { get; set; }
[Export("scene:willConnectToSession:options:")]
public void WillConnect(UIScene scene, UISceneSession session, UISceneConnectionOptions connectionOptions)
{
// Use this method to optionally configure and attach the UIWindow 'Window' to the provided UIWindowScene 'scene'.
// Since we are not using a storyboard, the 'Window' property needs to be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see UIApplicationDelegate 'GetConfiguration' instead).
if (scene is UIWindowScene windowScene)
{
Window ??= new UIWindow(windowScene);
var vc = new UIViewController();
vc.View!.BackgroundColor = UIColor.SystemBackground;
webView = new WKWebView(CGRect.Empty, new WKWebViewConfiguration());
webView.TranslatesAutoresizingMaskIntoConstraints = false;
vc.View.AddSubview(webView);
NSLayoutConstraint.ActivateConstraints(new[]
{
webView.TopAnchor.ConstraintEqualTo(vc.View.SafeAreaLayoutGuide.TopAnchor),
webView.LeadingAnchor.ConstraintEqualTo(vc.View.LeadingAnchor),
webView.TrailingAnchor.ConstraintEqualTo(vc.View.TrailingAnchor),
webView.BottomAnchor.ConstraintEqualTo(vc.View.SafeAreaLayoutGuide.BottomAnchor),
});
// Toolbar items
var spinner = new UIActivityIndicatorView(UIActivityIndicatorViewStyle.Medium);
var spinnerItem = new UIBarButtonItem(spinner);
var onlineOfflineSwitch = new UISwitch { On = true };
var fightButton = new UIBarButtonItem("Fight!", UIBarButtonItemStyle.Plain, (s, e) =>
{
try {
_ = Task.Run(async () =>
{
InvokeOnMainThread(() =>
{
vc.NavigationItem.RightBarButtonItem = spinnerItem;
spinner.StartAnimating();
});
bool offline = !onlineOfflineSwitch.On;
string filePath = await Renderer.Display(offline);
InvokeOnMainThread(() =>
{
spinner.StopAnimating();
vc.NavigationItem.RightBarButtonItem = null;
if (!string.IsNullOrEmpty(filePath))
{
webView.LoadFileUrl(new NSUrl(filePath, false), new NSUrl(Path.GetDirectoryName(filePath)!, false));
}
});
});
}
catch (Exception ex)
{
spinner.StopAnimating();
SetHTML("<html>" + System.Security.SecurityElement.Escape(ex.Message) + "</html>");
}
});
var shareButton = new UIBarButtonItem(UIBarButtonSystemItem.Action, (s, e) =>
{
FileSender.SendFile(Renderer.TempXlsPath, (UIBarButtonItem)s!);
});
var switchLabel = new UILabel { Text = "Online", Font = UIFont.SystemFontOfSize(14) };
var switchStack = new UIStackView(new UIView[] { switchLabel, onlineOfflineSwitch })
{
Axis = UILayoutConstraintAxis.Horizontal,
Spacing = 6,
Alignment = UIStackViewAlignment.Center
};
onlineOfflineSwitch.ValueChanged += (s, e) =>
{
switchLabel.Text = onlineOfflineSwitch.On ? "Online" : "Offline";
};
var switchItem = new UIBarButtonItem(switchStack);
var flexSpace = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace);
vc.ToolbarItems = new[] { fightButton, flexSpace, switchItem, flexSpace, shareButton };
var nav = new UINavigationController(vc);
nav.ToolbarHidden = false;
Window.RootViewController = nav;
Window.MakeKeyAndVisible();
}
}
void SetHTML(string html)
{
webView?.LoadHtmlString(html, new NSUrl(""));
}
[Export("sceneDidDisconnect:")]
public void DidDisconnect(UIScene scene)
{
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see UIApplicationDelegate `DidDiscardSceneSessions` instead).
}
[Export("sceneDidBecomeActive:")]
public void DidBecomeActive(UIScene scene)
{
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
[Export("sceneWillResignActive:")]
public void WillResignActive(UIScene scene)
{
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
[Export("sceneWillEnterForeground:")]
public void WillEnterForeground(UIScene scene)
{
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
[Export("sceneDidEnterBackground:")]
public void DidEnterBackground(UIScene scene)
{
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
}