Application setup and DI

Configure services, configuration, logging, the root shell, lifecycle callbacks, and app-wide appearance.

Every SkeleKit app starts with SkeleApplication.CreateBuilder(). The builder collects services, page registrations, app-wide settings, and exactly one root shell before creating the application.

Main.cs
using Microsoft.Extensions.DependencyInjection;
using MyApp;
using SkeleKit;

SkeleApplication.CreateBuilder()
    .UseServices(services =>
    {
        services.AddSingleton<IAccountStore, AccountStore>();
        services.AddTransient<HomeViewModel>();
        services.AddTransient<SettingsViewModel>();
    })
    .UseTheme(theme => theme.Tint(Colors.Indigo))
    .Stack<HomeView>()
    .Build()
    .Run(args);

Build() is supplied by the page source generator. It applies generated [Page] registrations, then creates the application and service provider. Run(args) starts the native iOS main loop.

Choose a shell

Configure one of the following:

  • SinglePage<TView>() hosts one page without navigation chrome.
  • Stack<TView>() creates a navigation controller with the given root page.
  • Tabs(...) gives every primary tab its own navigation stack.
  • SplitView(...) arranges two or three navigation stacks in a native split view.

Calling none of them is an error. The shell is app-level structure; individual pages can still present modals and use the system services.

See tabs and sidebar for the tab-shell builder, split views for column layouts, and navigation stacks for page-to-page navigation.

Services

UseServices exposes the normal Microsoft.Extensions.DependencyInjection service collection. Use singleton and transient registrations as usual. SkeleKit does not create a dependency-injection scope per page or navigation operation, so a scoped service resolved from the app's root provider effectively lives with that provider; do not use scoped lifetime when you expect page-local state.

SkeleKit adds these services when the application is built:

Every View exposes the same services through its protected Navigator, Sharer, SystemPicker, Haptics, and Mailer properties. Use those when an interaction belongs to view code, including custom controls, collection templates, and tab accessories.

Registered ViewModels receive their dependencies through ordinary constructors. A typed page then receives its ViewModel:

public sealed class AccountViewModel(IAccountStore accounts)
{
    public IAccountStore Accounts { get; } = accounts;
}

[Page]
public sealed class AccountView : ContentView<AccountViewModel>
{
    public AccountView(AccountViewModel viewModel) : base(viewModel)
    {
        Content = new Label { Text = Bind(vm => vm.Accounts.CurrentName) };
    }
}

The built provider is available through SkeleApplication.Current.Services after startup. Prefer the protected View properties in view code and constructor injection everywhere else. Resolve application-specific services directly when app-level accessory views or callbacks are created outside page construction.

Configuration and options

ConfigureAppConfiguration adds providers to one shared IConfiguration instance. SkeleKit does not choose a source; the app can use JSON, in-memory values, remote configuration, or any other Microsoft configuration provider.

.ConfigureAppConfiguration(configuration =>
{
    configuration.AddJsonStream(OpenBundledConfiguration());
})
.UseServices(services =>
{
    services.AddOptions<ApiOptions>()
        .BindConfiguration("Api");
})

Application services should normally consume the typed options:

public sealed class ApiClient(IOptions<ApiOptions> options)
{
    readonly ApiOptions configuration = options.Value;
}

Inject IConfiguration directly only when a service needs arbitrary sections or keys. Configuration providers and options binding extensions come from their corresponding Microsoft packages; for the example above, those are Microsoft.Extensions.Configuration.Json and Microsoft.Extensions.Options.ConfigurationExtensions.

Logging

SkeleKit registers Microsoft logging and sends its own diagnostics through ILogger. Add the providers and filters the app needs with ConfigureLogging:

.ConfigureLogging(logging =>
{
    logging.SetMinimumLevel(LogLevel.Information);
    logging.AddSimpleConsole(options => options.SingleLine = true);
})

SkeleKit does not add an output provider by default. AddSimpleConsole requires Microsoft.Extensions.Logging.Console; other providers, including Apple or rolling-file providers, can be added in the same callback. Application services receive ILogger<T> through constructor injection.

Lifecycle

Application lifecycle services complement the per-page appearance callbacks. Their methods are optional and may complete synchronously or asynchronously:

public sealed class AppLifecycle(AppConfig config) : IApplicationLifecycle
{
    public Task StartAsync()
    {
        config.ApplyAppearance();
        return Task.CompletedTask;
    }

    public Task EnterBackgroundAsync()
    {
        return config.SaveAsync();
    }
}

SkeleApplication.CreateBuilder()
    .UseLifecycle<AppLifecycle>()

UseLifecycle<T>() registers T as a singleton in the same dependency-injection container used by UseServices. Lifecycle services may use constructor injection and can also be resolved by their concrete type.

StartAsync runs once after SkeleApplication.Current is assigned and completes before UIKit starts. It can load data and set SkeleKit application properties such as Theme.Appearance and Theme.Tint, but it should stay short and must not access UIKit objects. Foreground services run in registration order. Background and stopping services run in reverse registration order.

public interface IApplicationLifecycle
{
    Task StartAsync() => Task.CompletedTask;
    Task EnterForegroundAsync() => Task.CompletedTask;
    Task EnterBackgroundAsync() => Task.CompletedTask;
    Task StopAsync() => Task.CompletedTask;
}

UIKit does not wait for asynchronous foreground, background, or stopping work. Keep these transitions short, save important state as it changes, and treat StopAsync as best-effort: iOS may terminate a suspended application without sending a stopping notification.

Appearance

Theme.Appearance sets the initial window appearance:

.UseTheme(theme => theme.Appearance(Appearance.System))

The running app can switch it later through SkeleApplication.Current.Theme.Appearance. System follows the device setting. Light and Dark set UIKit's override on every connected application window. Semantic Colors continue to resolve through UIKit for the selected appearance.

Tint

Theme.Tint supplies the inherited accent color for windows, navigation chrome, and controls:

.UseTheme(theme => theme.Tint(Colors.Indigo))

Changing SkeleApplication.Current.Theme.Tint reapplies it to visible page hosts and app-level tab accessories. The transition animates over a short duration unless the user has enabled Reduce Motion. A view or page can still set a more specific Tint or BarTint.

Theme, styles and image loading

UseTheme also carries the app-wide appearance defaults pages inherit: NavigationTitleStyle, TopScrollEdgeStyle, and TabBarMinimize. UseStyles registers reusable and implicit styles, while UseImageLoader replaces the default remote image loader. See styles and themes, tabs and sidebar, and Image custom loading for their full setup.