diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index f35492fb05..75ac8a0949 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -206,7 +206,7 @@
-
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index d12241609c..4d297703c3 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -7,7 +7,7 @@
21.0.08.2.0
- 8.4.0
+ 8.5.01.69.16
diff --git a/src/examples/Directory.Build.props b/src/examples/Directory.Build.props
index 25d87170fd..82d2a58426 100644
--- a/src/examples/Directory.Build.props
+++ b/src/examples/Directory.Build.props
@@ -6,6 +6,12 @@
the root defaults the value and derives $(EnablePublicApiBaseline) and the analyzer
reference from it, so setting it afterwards leaves tracking half on. -->
false
+
+
+ false
+ false
@@ -14,5 +20,7 @@
false14.0
+ false
+ false
diff --git a/src/examples/Documentation/Pages/commands/BookReceiptPrinter.cs b/src/examples/Documentation/Pages/commands/BookReceiptPrinter.cs
new file mode 100644
index 0000000000..2160d9bef0
--- /dev/null
+++ b/src/examples/Documentation/Pages/commands/BookReceiptPrinter.cs
@@ -0,0 +1,22 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.Commands;
+
+///
+/// A receipt printer that watches a borrow command's results directly, by implementing
+/// itself, rather than through an operator such as Subscribe(Action<T>).
+///
+public sealed class BookReceiptPrinter : IObserver
+{
+ ///
+ public void OnNext(Book value) => Console.WriteLine($"Printed a receipt for {value.Title}");
+
+ ///
+ public void OnError(Exception error) => Console.WriteLine($"Printer jammed: {error.Message}");
+
+ ///
+ public void OnCompleted() => Console.WriteLine("No more receipts to print");
+}
diff --git a/src/examples/Documentation/Pages/commands/CommandTypeExamples.cs b/src/examples/Documentation/Pages/commands/CommandTypeExamples.cs
index 4621a7a50e..4dc5e3aeb9 100644
--- a/src/examples/Documentation/Pages/commands/CommandTypeExamples.cs
+++ b/src/examples/Documentation/Pages/commands/CommandTypeExamples.cs
@@ -125,6 +125,48 @@ public static async Task DeriveFromCommandBase()
// cannot announce
}
+ ///
+ /// A witness that implements directly, such as , can
+ /// subscribe to a command's results the same way any other observer does.
+ ///
+ /// A task that completes once both books have been borrowed.
+ public static async Task SubscribeAWitnessToACommandsResults()
+ {
+ LibraryDesk desk = new();
+ BookReceiptPrinter witness = new();
+
+ using ReactiveCommand borrow = ReactiveCommand.Create(desk.Borrow);
+ using IDisposable subscription = borrow.Subscribe(witness);
+
+ _ = await borrow.Execute(1);
+ _ = await borrow.Execute(4);
+
+ // Output:
+ // Printed a receipt for Clean Code
+ // Printed a receipt for Refactoring
+ }
+
+ ///
+ /// Subscribing a witness through works whether the command was
+ /// built with a factory or, like , by hand; a display board only needs to
+ /// know a command produces string results, not which subclass built it.
+ ///
+ /// A task that completes once the announcement has posted to the board.
+ public static async Task SubscribeAWitnessThroughTheBaseType()
+ {
+ LibraryDesk desk = new();
+ DeskAnnouncementBoard board = new();
+ using DeskAnnouncementCommand closingSoon = new(desk, static () => "The desk closes in ten minutes.");
+
+ ReactiveCommandBase command = closingSoon;
+ using IDisposable subscription = command.Subscribe(board);
+
+ _ = await command.Execute();
+
+ // Output:
+ // Board: The desk closes in ten minutes.
+ }
+
///
/// Write your own command type from CombinedReactiveCommand<TParam, TResult>'s three constructors
/// when every combined command of a kind needs the same behaviour on each run;
diff --git a/src/examples/Documentation/Pages/commands/DeskAnnouncementBoard.cs b/src/examples/Documentation/Pages/commands/DeskAnnouncementBoard.cs
new file mode 100644
index 0000000000..6995ae049b
--- /dev/null
+++ b/src/examples/Documentation/Pages/commands/DeskAnnouncementBoard.cs
@@ -0,0 +1,22 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.Commands;
+
+///
+/// A display board that posts each announcement it receives directly, by implementing
+/// itself. It only needs a command that produces string results, not any particular command subclass.
+///
+public sealed class DeskAnnouncementBoard : IObserver
+{
+ ///
+ public void OnNext(string value) => Console.WriteLine($"Board: {value}");
+
+ ///
+ public void OnError(Exception error) => Console.WriteLine($"Board offline: {error.Message}");
+
+ ///
+ public void OnCompleted() => Console.WriteLine("Board cleared");
+}
diff --git a/src/examples/Documentation/Pages/commands/Program.cs b/src/examples/Documentation/Pages/commands/Program.cs
index 57d01aa20a..0e1311c286 100644
--- a/src/examples/Documentation/Pages/commands/Program.cs
+++ b/src/examples/Documentation/Pages/commands/Program.cs
@@ -74,6 +74,10 @@
await CommandTypeExamples.DeriveFromCommandBase();
+await CommandTypeExamples.SubscribeAWitnessToACommandsResults();
+
+await CommandTypeExamples.SubscribeAWitnessThroughTheBaseType();
+
await CommandTypeExamples.DeriveCombinedCommand();
await CombinedCommandMembersExamples.ReadCombinedCommandMembersDirectly();
diff --git a/src/examples/Documentation/Pages/platform-android/AbsenceActivity.cs b/src/examples/Documentation/Pages/platform-android/AbsenceActivity.cs
index 7bda16dfd6..e47e8de277 100644
--- a/src/examples/Documentation/Pages/platform-android/AbsenceActivity.cs
+++ b/src/examples/Documentation/Pages/platform-android/AbsenceActivity.cs
@@ -92,6 +92,19 @@ protected override async void OnCreate(Bundle? savedInstanceState)
await Task.Delay(TimeSpan.FromMilliseconds(100));
+ // This activity's own ActivityResult and StartActivityForResultAsync overloads, on the plain
+ // ReactiveActivity base rather than the AndroidX one MainActivity uses.
+ _ = ActivityResult.Subscribe(static result =>
+ TimetableLog.Info($"AbsenceActivity's own ActivityResult observed request {result.RequestCode}: {result.ResultCode}."));
+
+ Intent notePromptIntent = new(this, typeof(AbsenceNotePromptActivity));
+ notePromptIntent.PutExtra(SubjectExtra, ViewModel!.Subject);
+ (Android.App.Result ResultCode, Intent? Intent) notePromptByIntent = await StartActivityForResultAsync(notePromptIntent, 400);
+ TimetableLog.Info($"Note prompt (by intent) finished with {notePromptByIntent.ResultCode}.");
+
+ (Android.App.Result ResultCode, Intent? Intent) notePromptByType = await StartActivityForResultAsync(typeof(AbsenceNotePromptActivity), 401);
+ TimetableLog.Info($"Note prompt (by type) finished with {notePromptByType.ResultCode}.");
+
Intent resultIntent = new();
resultIntent.PutExtra(SubjectExtra, ViewModel!.Subject);
SetResult(Android.App.Result.Ok, resultIntent);
diff --git a/src/examples/Documentation/Pages/platform-android/AbsenceNotePromptActivity.cs b/src/examples/Documentation/Pages/platform-android/AbsenceNotePromptActivity.cs
new file mode 100644
index 0000000000..5c7e80b03a
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-android/AbsenceNotePromptActivity.cs
@@ -0,0 +1,35 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Android.Content;
+
+namespace ReactiveUI.Documentation.PlatformAndroid;
+
+/// Asks whether to add a note to an absence report. This activity predates AndroidX, like
+/// : it derives from the plain , so
+/// can start it with that base's own ActivityResult and
+/// StartActivityForResultAsync members rather than the AndroidX ones already
+/// uses.
+[Activity(Label = "Add a note")]
+public sealed class AbsenceNotePromptActivity : ReactiveActivity
+{
+ ///
+ protected override async void OnCreate(Bundle? savedInstanceState)
+ {
+ base.OnCreate(savedInstanceState);
+ SetContentView(Resource.Layout.activity_absence_note_prompt);
+
+ string subject = Intent?.GetStringExtra(AbsenceActivity.SubjectExtra) ?? "this lesson";
+ TextView promptLabel = FindViewById(Resource.Id.notePromptLabel)!;
+ promptLabel.Text = $"Add a note about {subject}?";
+
+ await Task.Delay(TimeSpan.FromMilliseconds(100));
+
+ Intent resultIntent = new();
+ resultIntent.PutExtra(AbsenceActivity.SubjectExtra, subject);
+ SetResult(Android.App.Result.Ok, resultIntent);
+ Finish();
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-android/LessonDetailFragment.cs b/src/examples/Documentation/Pages/platform-android/LessonDetailFragment.cs
index a9372cfa78..864854431b 100644
--- a/src/examples/Documentation/Pages/platform-android/LessonDetailFragment.cs
+++ b/src/examples/Documentation/Pages/platform-android/LessonDetailFragment.cs
@@ -3,6 +3,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.
+using System.Reflection;
using Android.Views;
namespace ReactiveUI.Documentation.PlatformAndroid;
@@ -30,6 +31,13 @@ public sealed class LessonDetailFragment : AndroidX.ReactiveFragment();
+ TimetableLog.Info($"SubjectLabel's resource name override: {subjectAttribute?.ResourceNameOverride}.");
+
// A control the fragment fetches directly, rather than wiring to a property.
View? icon = view.GetControl(GetType().Assembly, "detailIcon");
TimetableLog.Info(icon is null ? "detailIcon was not found." : "detailIcon resolved directly with GetControl.");
diff --git a/src/examples/Documentation/Pages/platform-android/LessonNotesFragment.cs b/src/examples/Documentation/Pages/platform-android/LessonNotesFragment.cs
new file mode 100644
index 0000000000..fb3470c654
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-android/LessonNotesFragment.cs
@@ -0,0 +1,31 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Android.Views;
+
+namespace ReactiveUI.Documentation.PlatformAndroid;
+
+/// Shows a revision note for a lesson, hosted as an .
+/// Wires its own label with the two-argument, implicit-strategy overload of WireUpControls, rather than
+/// stating a strategy explicitly the way does.
+[System.Diagnostics.DebuggerDisplay("{ViewModel}")]
+public sealed class LessonNotesFragment : AndroidX.ReactiveFragment
+{
+ /// Gets or sets the label showing the revision note; wired by naming convention to notesLabel.
+ public TextView? NotesLabel { get; set; }
+
+ ///
+ public override View? OnCreateView(LayoutInflater? inflater, ViewGroup? container, Bundle? savedInstanceState)
+ {
+ View view = inflater!.Inflate(Resource.Layout.fragment_lesson_notes, container, false)
+ ?? throw new InvalidOperationException("Inflating fragment_lesson_notes produced no view.");
+
+ // The convenience overload: no resolve strategy to state, since Implicit is what most fragments want.
+ AndroidX.ControlFetcherMixins.WireUpControls(this, view);
+
+ NotesLabel!.Text = ViewModel is null ? "No notes yet." : $"Revise {ViewModel.Subject} before the next lesson.";
+ return view;
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-android/LessonViewHolder.cs b/src/examples/Documentation/Pages/platform-android/LessonViewHolder.cs
index 95f058924d..053776ee9e 100644
--- a/src/examples/Documentation/Pages/platform-android/LessonViewHolder.cs
+++ b/src/examples/Documentation/Pages/platform-android/LessonViewHolder.cs
@@ -36,6 +36,24 @@ public LessonViewHolder(View itemView)
_subscriptions.Add(LongClickedWithViewModel.Subscribe(static lesson => TimetableLog.Info($"Long-clicked lesson: {lesson?.Subject}.")));
_subscriptions.Add(Activated.Subscribe(static _ => TimetableLog.Info("Lesson row attached to the window.")));
_subscriptions.Add(Deactivated.Subscribe(static _ => TimetableLog.Info("Lesson row detached from the window.")));
+
+ // The IReactiveObject surface every reactive Android base class carries: Changed and Changing report every
+ // property this holder itself raises a notification for (here, ViewModel, whenever RecyclerView recycles
+ // this row onto a new item), and ThrownExceptions reports anything an OnNext handler on those streams throws.
+ _subscriptions.Add(Changed.Subscribe(static change => TimetableLog.Info($"Lesson row property changed: {change.PropertyName}.")));
+ _subscriptions.Add(Changing.Subscribe(static change => TimetableLog.Info($"Lesson row property changing: {change.PropertyName}.")));
+ _subscriptions.Add(ThrownExceptions.Subscribe(static error => TimetableLog.Info($"Lesson row reported an exception: {error.Message}.")));
+
+ // SuppressChangeNotifications pauses Changed/Changing for its duration - useful when several properties
+ // are about to be filled in at once and only the final state matters. AreChangeNotificationsEnabled()
+ // reports whether a suppression is currently active.
+ using (SuppressChangeNotifications())
+ {
+ TimetableLog.Info($"Change notifications enabled while suppressed: {AreChangeNotificationsEnabled()}.");
+ }
+
+ TimetableLog.Info($"Change notifications enabled once the suppression ends: {AreChangeNotificationsEnabled()}.");
+ TimetableLog.Info($"Lesson row view: {View.GetType().Name}.");
}
/// Gets or sets the label showing the lesson's subject.
diff --git a/src/examples/Documentation/Pages/platform-android/LessonsRecyclerAdapter.cs b/src/examples/Documentation/Pages/platform-android/LessonsRecyclerAdapter.cs
index c817c2257e..2591ce52af 100644
--- a/src/examples/Documentation/Pages/platform-android/LessonsRecyclerAdapter.cs
+++ b/src/examples/Documentation/Pages/platform-android/LessonsRecyclerAdapter.cs
@@ -15,6 +15,20 @@ namespace ReactiveUI.Documentation.PlatformAndroid;
[System.Diagnostics.DebuggerDisplay("ItemCount = {ItemCount}")]
public sealed class LessonsRecyclerAdapter(ObservableCollection lessons) : ReactiveRecyclerViewAdapter>(lessons)
{
+ /// The view type for a lesson held in the science lab, room 7.
+ private const int LabViewType = 1;
+
+ /// The view type for every other lesson.
+ private const int StandardViewType = 0;
+
+ /// Picks a view type for a lesson: the science lab (room 7) gets its own type, so
+ /// can tell a lab lesson apart from any other.
+ /// The position of the lesson in the list.
+ /// The lesson at that position, or if the position is out of range.
+ /// An ID identifying the view type to use for the lesson.
+ public override int GetItemViewType(int position, LessonViewModel? viewModel) =>
+ viewModel?.Room == "7" ? LabViewType : StandardViewType;
+
///
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
@@ -25,6 +39,11 @@ public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int
View itemView = inflater.Inflate(Resource.Layout.lesson_item, parent, false)
?? throw new InvalidOperationException("Inflating lesson_item produced no view.");
+ if (viewType == LabViewType)
+ {
+ TimetableLog.Info("Lesson row created for the science lab (room 7).");
+ }
+
return new LessonViewHolder(itemView);
}
}
diff --git a/src/examples/Documentation/Pages/platform-android/MainActivity.cs b/src/examples/Documentation/Pages/platform-android/MainActivity.cs
index 3fa21a0fa9..efa1117270 100644
--- a/src/examples/Documentation/Pages/platform-android/MainActivity.cs
+++ b/src/examples/Documentation/Pages/platform-android/MainActivity.cs
@@ -5,6 +5,7 @@
using System.Reflection;
using Android.Content;
+using Android.Hardware.Usb;
using AndroidX.RecyclerView.Widget;
namespace ReactiveUI.Documentation.PlatformAndroid;
@@ -50,7 +51,18 @@ protected override async void OnCreate(Bundle? savedInstanceState)
TimetableLog.Info($" {member.Name} -> resource '{member.GetResourceName()}'.");
}
- ViewModel = new TimetableViewModel();
+ // The IReactiveObject surface every reactive Android base class carries: Changed and Changing report every
+ // property change this activity itself raises (here, ViewModel), and ThrownExceptions reports anything an
+ // OnNext handler on those streams throws. SuppressChangeNotifications pauses Changed/Changing for its
+ // duration - useful while several properties are about to be filled in at once.
+ _subscriptions.Add(Changed.Subscribe(static change => TimetableLog.Info($"MainActivity property changed: {change.PropertyName}.")));
+ _subscriptions.Add(Changing.Subscribe(static change => TimetableLog.Info($"MainActivity property changing: {change.PropertyName}.")));
+ _subscriptions.Add(ThrownExceptions.Subscribe(static error => TimetableLog.Info($"MainActivity reported an exception: {error.Message}.")));
+
+ using (SuppressChangeNotifications())
+ {
+ ViewModel = new TimetableViewModel();
+ }
// GetOrientation() returns the display's current rotation by name, such as "Rotation0" for the natural orientation.
PlatformOperations platformOperations = new();
@@ -76,7 +88,11 @@ protected override async void OnCreate(Bundle? savedInstanceState)
_ = new LessonPeekHost(this, PeeksRow!, attachToRoot: true) { ViewModel = ViewModel.Lessons[2] };
LessonsRecyclerView!.SetLayoutManager(new LinearLayoutManager(this));
- LessonsRecyclerView!.SetAdapter(new LessonsRecyclerAdapter(ViewModel.Lessons));
+ LessonsRecyclerAdapter lessonsAdapter = new(ViewModel.Lessons);
+ LessonsRecyclerView!.SetAdapter(lessonsAdapter);
+ TimetableLog.Info($"Lessons adapter reports {lessonsAdapter.ItemCount} items.");
+
+ RequestAttendanceScannerPermission();
_subscriptions.Add(Activated.Subscribe(static _ => TimetableLog.Info("MainActivity activated.")));
_subscriptions.Add(Deactivated.Subscribe(static _ => TimetableLog.Info("MainActivity deactivated.")));
@@ -97,6 +113,28 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}
+ /// Some timetable devices use a USB barcode scanner to check students in as they arrive. If one is
+ /// already plugged in, asks the system for permission to talk to it.
+ private void RequestAttendanceScannerPermission()
+ {
+ if (GetSystemService(UsbService) is not UsbManager usbManager)
+ {
+ return;
+ }
+
+ foreach (UsbDevice device in usbManager.DeviceList?.Values ?? [])
+ {
+ _subscriptions.Add(usbManager.PermissionRequested(this, device)
+ .Subscribe(granted => TimetableLog.Info($"USB device {device.DeviceName} permission granted: {granted}.")));
+ }
+
+ foreach (UsbAccessory accessory in usbManager.GetAccessoryList() ?? [])
+ {
+ _subscriptions.Add(usbManager.PermissionRequested(this, accessory)
+ .Subscribe(granted => TimetableLog.Info($"USB accessory {accessory.Model} permission granted: {granted}.")));
+ }
+ }
+
/// Drives the whole demo end to end: pick a lesson, report an absence, show its detail, confirm the
/// absence, page the weekdays, then open notification settings.
/// A task that completes once every screen in the scenario has run.
@@ -139,6 +177,12 @@ private async Task RunScenario()
.CommitNowAllowingStateLoss();
TimetableLog.Info("Showing notification settings.");
+ LessonNotesFragment notesFragment = new() { ViewModel = firstLesson };
+ SupportFragmentManager!.BeginTransaction()!
+ .Replace(DetailContainer!.Id, notesFragment)!
+ .CommitNowAllowingStateLoss();
+ TimetableLog.Info($"Showing notes for {firstLesson.Subject}.");
+
TimetableLog.Info("=== Scenario complete ===");
}
}
diff --git a/src/examples/Documentation/Pages/platform-android/Resources/layout/activity_absence_note_prompt.xml b/src/examples/Documentation/Pages/platform-android/Resources/layout/activity_absence_note_prompt.xml
new file mode 100644
index 0000000000..5047b3c000
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-android/Resources/layout/activity_absence_note_prompt.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/src/examples/Documentation/Pages/platform-android/Resources/layout/fragment_lesson_notes.xml b/src/examples/Documentation/Pages/platform-android/Resources/layout/fragment_lesson_notes.xml
new file mode 100644
index 0000000000..e384e68fb3
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-android/Resources/layout/fragment_lesson_notes.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/src/examples/Documentation/Pages/platform-android/WeekdayPagerActivity.cs b/src/examples/Documentation/Pages/platform-android/WeekdayPagerActivity.cs
index 108d079d1d..d328550803 100644
--- a/src/examples/Documentation/Pages/platform-android/WeekdayPagerActivity.cs
+++ b/src/examples/Documentation/Pages/platform-android/WeekdayPagerActivity.cs
@@ -102,7 +102,10 @@ private View CreatePage(WeekdayViewModel weekday, ViewGroup parent)
{
_ = weekday;
WeekdayViewHost host = new(this, parent);
- View view = host.ToView()!;
+
+ // LayoutViewHost's implicit operator to View, used here instead of the ToView() alternate.
+ View? convertedView = host;
+ View view = convertedView ?? throw new InvalidOperationException("WeekdayViewHost converted to a null View.");
WeekdayViewHost? typedHost = view.GetViewHost();
ILayoutViewHost? untypedHost = view.GetViewHost();
diff --git a/src/examples/Documentation/Pages/platform-apple/LinuxPlaceholder/Program.cs b/src/examples/Documentation/Pages/platform-apple/LinuxPlaceholder/Program.cs
new file mode 100644
index 0000000000..ccb5263961
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/LinuxPlaceholder/Program.cs
@@ -0,0 +1,7 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+// The iOS and macOS workloads exist only on Windows and macOS. Build this project there to compile the examples.
+Console.WriteLine("The Apple examples build on Windows and macOS.");
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/Book.cs b/src/examples/Documentation/Pages/platform-apple/Shared/Book.cs
new file mode 100644
index 0000000000..36e41a7e41
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/Book.cs
@@ -0,0 +1,40 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A book the library owns. Bound labels and controls refresh whenever or changes.
+[System.Diagnostics.DebuggerDisplay("Book {Title} IsOnLoan = {IsOnLoan}")]
+public sealed class Book : ReactiveObject
+{
+ /// Initializes a new instance of the class.
+ /// The book's title.
+ /// The book's author.
+ public Book(string title, string author)
+ {
+ Title = title;
+ Author = author;
+ }
+
+ /// Gets the book's title.
+ public string Title { get; }
+
+ /// Gets the book's author.
+ public string Author { get; }
+
+ /// Gets or sets a value indicating whether a member currently has the book on loan.
+ public bool IsOnLoan
+ {
+ get;
+ set => this.RaiseAndSetIfChanged(ref field, value);
+ }
+
+ /// Gets or sets the book's star rating, from 0 to 5.
+ public int Rating
+ {
+ get;
+ set => this.RaiseAndSetIfChanged(ref field, value);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppState.cs b/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppState.cs
new file mode 100644
index 0000000000..49d6f1d1f2
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppState.cs
@@ -0,0 +1,15 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The application state saves under Application Support and
+/// restores from it, so a relaunched app picks up where the last one left off.
+[System.Diagnostics.DebuggerDisplay("{LastViewedBook}")]
+public sealed class LibraryAppState
+{
+ /// Gets or sets the title of the book the member last looked at.
+ public string LastViewedBook { get; set; } = string.Empty;
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppStateJsonContext.cs b/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppStateJsonContext.cs
new file mode 100644
index 0000000000..d7d2f88171
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/LibraryAppStateJsonContext.cs
@@ -0,0 +1,15 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Text.Json.Serialization;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// Source-generated serialization metadata for , so
+/// 's trim- and AOT-safe LoadState{T}(JsonTypeInfo{T}) and
+/// SaveState{T}(T, JsonTypeInfo{T}) overloads never need reflection-based serialization.
+[JsonSerializable(typeof(LibraryAppState))]
+[System.Diagnostics.DebuggerDisplay("LibraryAppStateJsonContext")]
+public sealed partial class LibraryAppStateJsonContext : JsonSerializerContext;
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/Member.cs b/src/examples/Documentation/Pages/platform-apple/Shared/Member.cs
new file mode 100644
index 0000000000..100d4f1544
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/Member.cs
@@ -0,0 +1,12 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A library member who can borrow books.
+/// The member's card number.
+/// The member's name.
+[System.Diagnostics.DebuggerDisplay("Member {Name} ({Id})")]
+public sealed record Member(string Id, string Name);
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/BookCatalogViewModel.cs b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/BookCatalogViewModel.cs
new file mode 100644
index 0000000000..867a0bc2de
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/BookCatalogViewModel.cs
@@ -0,0 +1,59 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Collections.ObjectModel;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The catalog page: every book the library owns, plus the members who can borrow one.
+[System.Diagnostics.DebuggerDisplay("BookCatalogViewModel Books = {Books.Count}")]
+public sealed class BookCatalogViewModel : ReactiveObject, IRoutableViewModel, IDisposable
+{
+ /// The members who can borrow a book, carried forward to the loan page.
+ private readonly IReadOnlyList _members;
+
+ /// Initializes a new instance of the class.
+ /// The shell that owns the router.
+ /// The library's catalog.
+ /// The members who can borrow a book.
+ public BookCatalogViewModel(IScreen hostScreen, IReadOnlyList books, IReadOnlyList members)
+ {
+ HostScreen = hostScreen;
+ Books = books as ObservableCollection ?? new ObservableCollection(books);
+ _members = members;
+
+ IObservable canLoan = this.WhenAnyValue(
+ static x => x.SelectedBook,
+ static book => book is not null && !book.IsOnLoan);
+
+ OpenLoan = ReactiveCommand.CreateFromObservable(
+ () => HostScreen.Router.Navigate.Execute(new LoanViewModel(HostScreen, SelectedBook!, _members)),
+ canLoan);
+ }
+
+ ///
+ public string UrlPathSegment => "catalog";
+
+ ///
+ public IScreen HostScreen { get; }
+
+ /// Gets the library's catalog. A reactive table or collection source binds to this directly: because it
+ /// implements , the source refreshes the
+ /// view whenever a book is added or removed.
+ public ObservableCollection Books { get; }
+
+ /// Gets or sets the book the member picked from the catalog.
+ public Book? SelectedBook
+ {
+ get;
+ set => this.RaiseAndSetIfChanged(ref field, value);
+ }
+
+ /// Gets the command that opens the loan page for .
+ public ReactiveCommand OpenLoan { get; }
+
+ ///
+ public void Dispose() => OpenLoan.Dispose();
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LibraryShellViewModel.cs b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LibraryShellViewModel.cs
new file mode 100644
index 0000000000..fa46532a46
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LibraryShellViewModel.cs
@@ -0,0 +1,17 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The library app's shell. It owns the router that RoutedViewHost follows on iOS.
+[System.Diagnostics.DebuggerDisplay("Pages = {Router.NavigationStack.Count}")]
+public sealed class LibraryShellViewModel : ReactiveObject, IScreen
+{
+ ///
+ public RoutingState Router { get; } = new();
+
+ /// Gets the title the macOS window shows for the shell.
+ public string Title => "Library";
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LoanViewModel.cs b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LoanViewModel.cs
new file mode 100644
index 0000000000..9244cc5c5e
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/LoanViewModel.cs
@@ -0,0 +1,66 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The loan page: pick a member for , then confirm or cancel.
+[System.Diagnostics.DebuggerDisplay("LoanViewModel Book = {Book.Title}")]
+public sealed class LoanViewModel : ReactiveObject, IRoutableViewModel, IDisposable
+{
+ /// Initializes a new instance of the class.
+ /// The shell that owns the router.
+ /// The book being loaned.
+ /// The members who can borrow the book.
+ public LoanViewModel(IScreen hostScreen, Book book, IReadOnlyList members)
+ {
+ HostScreen = hostScreen;
+ Book = book;
+ Members = members;
+
+ IObservable canConfirm = this.WhenAnyValue(static x => x.SelectedMember).Select(static member => member is not null);
+
+ ConfirmLoan = ReactiveCommand.CreateFromObservable(
+ () =>
+ {
+ Book.IsOnLoan = true;
+ return HostScreen.Router.NavigateBack.Execute();
+ },
+ canConfirm);
+
+ Cancel = ReactiveCommand.CreateFromObservable(() => HostScreen.Router.NavigateBack.Execute());
+ }
+
+ ///
+ public string UrlPathSegment => "loan";
+
+ ///
+ public IScreen HostScreen { get; }
+
+ /// Gets the book being loaned.
+ public Book Book { get; }
+
+ /// Gets the members who can borrow the book. The view shows a row per member.
+ public IReadOnlyList Members { get; }
+
+ /// Gets or sets the member the librarian picked from .
+ public Member? SelectedMember
+ {
+ get;
+ set => this.RaiseAndSetIfChanged(ref field, value);
+ }
+
+ /// Gets the command that marks the book on loan and returns to the catalog.
+ public ReactiveCommand ConfirmLoan { get; }
+
+ /// Gets the command that returns to the catalog without loaning the book.
+ public ReactiveCommand Cancel { get; }
+
+ ///
+ public void Dispose()
+ {
+ ConfirmLoan.Dispose();
+ Cancel.Dispose();
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/MembersViewModel.cs b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/MembersViewModel.cs
new file mode 100644
index 0000000000..ee75b9a882
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/Shared/ViewModels/MembersViewModel.cs
@@ -0,0 +1,38 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Collections.ObjectModel;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The members tab: every library member, as the change-set source a reactive table or collection source reads from.
+[System.Diagnostics.DebuggerDisplay("MembersViewModel Members = {Members.Count}")]
+public sealed class MembersViewModel : ReactiveObject, IRoutableViewModel
+{
+ /// Initializes a new instance of the class.
+ /// The shell that owns the router.
+ /// Every library member.
+ public MembersViewModel(IScreen hostScreen, IReadOnlyList members)
+ {
+ HostScreen = hostScreen;
+ Members = members as ObservableCollection ?? new ObservableCollection(members);
+ MemberCount = members.Count;
+ }
+
+ ///
+ public string UrlPathSegment => "members";
+
+ ///
+ public IScreen HostScreen { get; }
+
+ /// Gets every library member. A reactive table or collection source binds to this directly: because it
+ /// implements , the source refreshes the
+ /// view whenever a member is added or removed.
+ public ObservableCollection Members { get; }
+
+ /// Gets how many members the library has. A plain so a one-way binding can observe
+ /// it directly, rather than chaining through the non-reactive list.
+ public int MemberCount { get; }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/AppDelegate.cs b/src/examples/Documentation/Pages/platform-apple/iOS/AppDelegate.cs
new file mode 100644
index 0000000000..3307657ea5
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/AppDelegate.cs
@@ -0,0 +1,84 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Foundation;
+using ReactiveUI.Builder;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The app's UIApplicationDelegate: builds ReactiveUI, wires up state suspension, and shows the shell.
+[Register(nameof(AppDelegate))]
+[System.Diagnostics.DebuggerDisplay("AppDelegate")]
+public sealed class AppDelegate : UIApplicationDelegate
+{
+ /// Keeps UIKit lifecycle callbacks translated into suspend/resume signals for as long as the process lives.
+ private AutoSuspendHelper? _autoSuspendHelper;
+
+ ///
+ public override UIWindow? Window { get; set; }
+
+ ///
+ public override bool FinishedLaunching(UIApplication application, NSDictionary? launchOptions)
+ {
+ ReactiveUIBuilder builder = RxAppBuilder.CreateReactiveUIBuilder();
+ _ = builder.WithPlatformModule().BuildApp();
+
+ _autoSuspendHelper = new AutoSuspendHelper(this);
+ _autoSuspendHelper.FinishedLaunching(application, launchOptions!);
+ Console.WriteLine($"FinishedLaunching captured {_autoSuspendHelper.LaunchOptions?.Count ?? 0} launch option(s).");
+
+ RxSuspension.SuspensionHost.CreateNewAppState = static () => new LibraryAppState();
+
+ AppSupportJsonSuspensionDriver driver = new("Library");
+ RxSuspension.SuspensionHost.SetupDefaultSuspendResume(driver);
+
+ _ = driver.SaveState(new LibraryAppState { LastViewedBook = "Pride and Prejudice" }, LibraryAppStateJsonContext.Default.LibraryAppState)
+ .Subscribe(static _ => Console.WriteLine("AppSupportJsonSuspensionDriver saved the app state."));
+ _ = driver.LoadState(LibraryAppStateJsonContext.Default.LibraryAppState)
+ .Subscribe(
+ static state => Console.WriteLine($"AppSupportJsonSuspensionDriver loaded state for {state?.LastViewedBook}."),
+ static error => Console.WriteLine($"AppSupportJsonSuspensionDriver had nothing to load yet: {error.Message}."));
+ _ = driver.InvalidateState().Subscribe(static _ => Console.WriteLine("AppSupportJsonSuspensionDriver invalidated the saved state."));
+
+ // GetOrientation() returns the device's current rotation by name, such as "Portrait".
+ PlatformOperations platformOperations = new();
+ string? orientation = platformOperations.GetOrientation();
+ Console.WriteLine($"Device orientation: {orientation}.");
+
+ if (OperatingSystem.IsIOSVersionAtLeast(26))
+ {
+ // iOS 26 deprecates window construction outside a UIWindowScene; this sample targets 15 through 25 and
+ // stays on the classic, scene-less API below that ceiling.
+ throw new PlatformNotSupportedException("This sample targets iOS 15 through 25.");
+ }
+
+ UIWindow window = new(UIScreen.MainScreen.Bounds);
+ window.RootViewController = LibraryComposition.CreateRootViewController();
+ window.MakeKeyAndVisible();
+ Window = window;
+
+ return true;
+ }
+
+ ///
+ public override void OnActivated(UIApplication application) =>
+ _autoSuspendHelper?.OnActivated(application);
+
+ ///
+ public override void DidEnterBackground(UIApplication application) =>
+ _autoSuspendHelper?.DidEnterBackground(application);
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _autoSuspendHelper?.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookCoverPagerViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookCoverPagerViewController.cs
new file mode 100644
index 0000000000..3fcca1c50f
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookCoverPagerViewController.cs
@@ -0,0 +1,59 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// Pages through one per book, left to right.
+[System.Diagnostics.DebuggerDisplay("BookCoverPagerViewController")]
+public sealed class BookCoverPagerViewController : ReactivePageViewController, IUIPageViewControllerDataSource
+{
+ /// Initializes a new instance of the class with a scroll transition.
+ public BookCoverPagerViewController()
+ : base(UIPageViewControllerTransitionStyle.Scroll, UIPageViewControllerNavigationOrientation.Horizontal)
+ {
+ }
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ DataSource = this;
+
+ _ = this.WhenActivated((Action onDispose) =>
+ {
+ _ = onDispose;
+ if (ViewModel!.Books.Count == 0)
+ {
+ return;
+ }
+
+ SetViewControllers([CreatePage(ViewModel.Books[0])], UIPageViewControllerNavigationDirection.Forward, false, null);
+ });
+ }
+
+ ///
+ public new UIViewController GetPreviousViewController(UIPageViewController pageViewController, UIViewController referenceViewController)
+ {
+ Book current = ((BookDetailViewController)referenceViewController).ViewModel!;
+ int index = ViewModel!.Books.IndexOf(current) - 1;
+ return index >= 0 ? CreatePage(ViewModel.Books[index]) : null!;
+ }
+
+ ///
+ public new UIViewController GetNextViewController(UIPageViewController pageViewController, UIViewController referenceViewController)
+ {
+ Book current = ((BookDetailViewController)referenceViewController).ViewModel!;
+ int index = ViewModel!.Books.IndexOf(current) + 1;
+ return index < ViewModel.Books.Count ? CreatePage(ViewModel.Books[index]) : null!;
+ }
+
+ /// Creates the detail page for one book, with its ViewModel set before the pager displays it.
+ /// The book the page shows.
+ /// The page.
+ private static BookDetailViewController CreatePage(Book book) => new() { ViewModel = book };
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookDetailViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookDetailViewController.cs
new file mode 100644
index 0000000000..3776e79aca
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookDetailViewController.cs
@@ -0,0 +1,87 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One book's detail: cover, title, author, and a star rating a librarian can adjust.
+[System.Diagnostics.DebuggerDisplay("BookDetailViewController")]
+public sealed class BookDetailViewController : ReactiveViewController
+{
+ /// Hosts the cover and rating controls, added once the view model is set on them first.
+ private readonly UIStackView _layout;
+
+ /// Initializes a new instance of the class.
+ public BookDetailViewController() =>
+ _layout = new UIStackView([TitleLabel, AuthorLabel])
+ {
+ Axis = UILayoutConstraintAxis.Vertical,
+ Alignment = UIStackViewAlignment.Center,
+ Spacing = 12,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+
+ /// Gets the label showing the book's title. Internal so the binding source generator can observe it.
+ internal UILabel TitleLabel { get; } = new() { Font = UIFont.PreferredTitle1! };
+
+ /// Gets the label showing the book's author. Internal so the binding source generator can observe it.
+ internal UILabel AuthorLabel { get; } = new() { Font = UIFont.PreferredBody!, TextColor = UIColor.SecondaryLabel };
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ View!.BackgroundColor = UIColor.SystemBackground;
+ View!.AddSubview(_layout);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ _layout.CenterXAnchor.ConstraintEqualTo(View!.CenterXAnchor),
+ _layout.TopAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TopAnchor, 24),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ Title = ViewModel!.Title;
+
+ // Setting ViewModel before adding each control to the stack means it already has a book to bind to the
+ // moment it gains a superview and its own activation runs.
+ BookCoverImageView cover = new() { ViewModel = ViewModel, TranslatesAutoresizingMaskIntoConstraints = false };
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ cover.WidthAnchor.ConstraintEqualTo(120),
+ cover.HeightAnchor.ConstraintEqualTo(160),
+ ]);
+ _layout.InsertArrangedSubview(cover, 0);
+
+ StarRatingControl rating = new() { ViewModel = ViewModel };
+ _layout.AddArrangedSubview(rating);
+
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.Text));
+ d(this.OneWayBind(ViewModel, static vm => vm.Author, static v => v.AuthorLabel.Text));
+ d(new ActionDisposable(() =>
+ {
+ cover.RemoveFromSuperview();
+ cover.Dispose();
+ rating.RemoveFromSuperview();
+ rating.Dispose();
+ }));
+ });
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ AuthorLabel.Dispose();
+ _layout.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookListViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookListViewController.cs
new file mode 100644
index 0000000000..a47e996618
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookListViewController.cs
@@ -0,0 +1,76 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using CoreGraphics;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The catalog page: one row per book, and a button that starts a loan for the selected one.
+[System.Diagnostics.DebuggerDisplay("BookListViewController")]
+public sealed class BookListViewController : ReactiveViewController
+{
+ /// The stack that hosts one per book. A real catalog would use a reactive table source instead.
+ private readonly UIStackView _rows = new() { Axis = UILayoutConstraintAxis.Vertical, Spacing = 8, TranslatesAutoresizingMaskIntoConstraints = false };
+
+ /// Gets the button that opens the loan page for the selected book. Internal for the binding source generator.
+ internal UIButton LoanButton { get; } = UIButton.FromType(UIButtonType.System);
+
+ /// Gets the plain table listing books currently on loan, hosted below the button.
+ internal LoanBoardView LoanBoard { get; } = new(CGRect.Empty);
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ Title = "Catalog";
+ View!.BackgroundColor = UIColor.SystemBackground;
+ LoanButton.SetTitle("Loan selected book", UIControlState.Normal);
+ LoanBoard.ViewModel = ViewModel;
+ LoanBoard.HeightAnchor.ConstraintEqualTo(140).Active = true;
+
+ UIStackView layout = new([_rows, LoanButton, LoanBoard])
+ {
+ Axis = UILayoutConstraintAxis.Vertical,
+ Spacing = 16,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+ View!.AddSubview(layout);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ layout.LeadingAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.LeadingAnchor, 16),
+ layout.TrailingAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TrailingAnchor, -16),
+ layout.TopAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TopAnchor, 16),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ foreach (Book book in ViewModel!.Books)
+ {
+ // The frame constructor pre-sizes the row; Auto Layout resizes it once ViewModel bindings run.
+ BookRowView row = new(CGRect.Empty) { ViewModel = book };
+ UITapGestureRecognizer tap = new(() => ViewModel!.SelectedBook = book);
+ row.AddGestureRecognizer(tap);
+ _rows.AddArrangedSubview(row);
+ }
+
+ d(this.BindCommand(ViewModel, static vm => vm.OpenLoan, static v => v.LoanButton));
+ });
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _rows.Dispose();
+ LoanButton.Dispose();
+ LoanBoard.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfCollectionViewSource.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfCollectionViewSource.cs
new file mode 100644
index 0000000000..491cdab636
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfCollectionViewSource.cs
@@ -0,0 +1,37 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Foundation;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A that also supplies the shelf's section header. The
+/// ReactiveCollectionViewSourceExtensions.BindTo family always builds a plain ,
+/// which has no header hook of its own, so a grid that needs one subclasses the source directly instead, the same
+/// way that extension method does internally.
+internal sealed class BookShelfCollectionViewSource : ReactiveCollectionViewSource
+{
+ /// The reuse identifier the shelf registers under.
+ internal static readonly NSString HeaderKey = new(nameof(ShelfHeaderView));
+
+ /// The catalog the header's book count comes from.
+ private readonly BookCatalogViewModel _catalog;
+
+ /// Initializes a new instance of the class.
+ /// The grid this source drives.
+ /// The catalog the header's book count comes from.
+ internal BookShelfCollectionViewSource(UICollectionView collectionView, BookCatalogViewModel catalog)
+ : base(collectionView) =>
+ _catalog = catalog;
+
+ ///
+ public override UICollectionReusableView GetViewForSupplementaryElement(UICollectionView collectionView, NSString elementKind, NSIndexPath indexPath)
+ {
+ ShelfHeaderView header = (ShelfHeaderView)collectionView.DequeueReusableSupplementaryView(UICollectionElementKindSection.Header, HeaderKey, indexPath);
+ header.ViewModel = _catalog;
+ return header;
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfViewController.cs
new file mode 100644
index 0000000000..36e24744c6
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/BookShelfViewController.cs
@@ -0,0 +1,62 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using CoreGraphics;
+using Foundation;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The shelf tab: every book as a cover tile in a grid.
+[System.Diagnostics.DebuggerDisplay("BookShelfViewController")]
+public sealed class BookShelfViewController : ReactiveCollectionViewController
+{
+ /// The reuse identifier every cover shares; the shelf never needs a different cell per book.
+ private static readonly NSString CoverCellKey = new(nameof(BookCoverCell));
+
+ /// Initializes a new instance of the class with a two-column grid layout.
+ public BookShelfViewController()
+ : base(new UICollectionViewFlowLayout
+ {
+ ItemSize = new CGSize(140, 90),
+ MinimumInteritemSpacing = 12,
+ MinimumLineSpacing = 12,
+ SectionInset = new UIEdgeInsets(12, 12, 12, 12),
+ HeaderReferenceSize = new CGSize(0, 32),
+ })
+ {
+ }
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ Title = "Shelf";
+ CollectionView!.BackgroundColor = UIColor.SystemBackground;
+ CollectionView.RegisterClassForCell(typeof(BookCoverCell), CoverCellKey);
+ CollectionView.RegisterClassForSupplementaryView(typeof(ShelfHeaderView), UICollectionElementKindSection.Header, BookShelfCollectionViewSource.HeaderKey);
+
+ _ = this.WhenActivated(d =>
+ {
+ CollectionViewSectionInformation section = new(
+ ViewModel!.Books,
+ static _ => CoverCellKey,
+ static cell => Console.WriteLine($"Initializing a {cell.GetType().Name}."));
+ IReadOnlyList> sections = [section];
+
+ BookShelfCollectionViewSource source = new(CollectionView!, ViewModel);
+ source.Data = sections;
+ CollectionView!.Source = source;
+
+ CollectionViewSectionInformation readBack = source.Data[0];
+ Console.WriteLine(
+ $"Shelf has {source.Data.Count} section(s); every cover shares one reuse key: {readBack.CellKeySelector is not null}.");
+
+ d(source);
+ d(source.ElementSelected.Subscribe(static item => Console.WriteLine($"Selected cover {((Book)item!).Title}.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/LibraryComposition.cs b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryComposition.cs
new file mode 100644
index 0000000000..7f3422e1c3
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryComposition.cs
@@ -0,0 +1,44 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Collections.ObjectModel;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// Builds the sample library data and the app's root view controller.
+internal static class LibraryComposition
+{
+ /// Creates the root view controller: a split view on iPad, a tab bar everywhere else.
+ /// The root view controller for .
+ internal static UIViewController CreateRootViewController()
+ {
+ Member ada = new("M-1", "Ada");
+ Member grace = new("M-2", "Grace");
+ ObservableCollection members = [ada, grace];
+
+ ObservableCollection books =
+ [
+ new("Pride and Prejudice", "Jane Austen"),
+ new("The Hobbit", "J. R. R. Tolkien"),
+ new("Dune", "Frank Herbert"),
+ ];
+
+ LibraryShellViewModel shell = new();
+ BookCatalogViewModel catalog = new(shell, books, members);
+ MembersViewModel membersViewModel = new(shell, members);
+
+ if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad)
+ {
+ return new LibrarySplitViewController(shell, catalog);
+ }
+
+ DefaultViewLocator catalogViewLocator = new();
+ catalogViewLocator.Map();
+ catalogViewLocator.Map();
+
+ return new LibraryTabBarController(shell, catalogViewLocator, catalog, membersViewModel);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/LibrarySplitViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/LibrarySplitViewController.cs
new file mode 100644
index 0000000000..661be376b4
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/LibrarySplitViewController.cs
@@ -0,0 +1,52 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The app's root on iPad: the catalog as the master column, a as the detail.
+[System.Diagnostics.DebuggerDisplay("LibrarySplitViewController")]
+public sealed class LibrarySplitViewController : ReactiveSplitViewController
+{
+ /// Shows the detail view for whichever book the master column selects.
+ private readonly ViewModelViewHost _detailHost;
+
+ /// Initializes a new instance of the class.
+ /// The shell view model.
+ /// The catalog shown in the master column.
+ public LibrarySplitViewController(LibraryShellViewModel shell, BookCatalogViewModel catalog)
+ {
+ ViewModel = shell;
+ PreferredDisplayMode = UISplitViewControllerDisplayMode.OneBesideSecondary;
+
+ DefaultViewLocator detailLocator = new();
+ detailLocator.Map();
+ _detailHost = new ViewModelViewHost
+ {
+ DefaultContent = new UIViewController(),
+ ViewLocator = detailLocator,
+ };
+
+ BookListViewController master = new() { ViewModel = catalog };
+ ViewControllers = [master, _detailHost];
+
+ _ = this.WhenActivated(d =>
+ d(catalog.WhenAnyValue(static vm => vm.SelectedBook)
+ .WhereNotNull()
+ .Subscribe(book => _detailHost.ViewModel = book)));
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _detailHost.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/LibraryTabBarController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryTabBarController.cs
new file mode 100644
index 0000000000..7e4dce285d
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/LibraryTabBarController.cs
@@ -0,0 +1,57 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The app's root on iPhone: a catalog tab, a members tab, and a cover-paging tab.
+[System.Diagnostics.DebuggerDisplay("LibraryTabBarController")]
+public sealed class LibraryTabBarController : ReactiveTabBarController
+{
+ /// Initializes a new instance of the class.
+ /// The shell that owns the catalog's router.
+ /// The view locator the catalog tab's uses to
+ /// resolve a view for each pushed view model.
+ /// The catalog view model shown first in the catalog tab.
+ /// The members view model shown in the members tab.
+ public LibraryTabBarController(LibraryShellViewModel shell, IViewLocator catalogViewLocator, BookCatalogViewModel catalog, MembersViewModel members)
+ {
+ ViewModel = shell;
+
+ RoutedViewHost catalogHost = new()
+ {
+ Router = shell.Router,
+ ViewLocator = catalogViewLocator,
+ };
+ catalogHost.TabBarItem = new UITabBarItem("Catalog", null, 0);
+ _ = shell.Router.Navigate.Execute(catalog).Subscribe();
+
+ MembersNavigationController membersNav = new(members)
+ {
+ TabBarItem = new UITabBarItem("Members", null, 1),
+ };
+
+ BookCoverPagerViewController coverPager = new()
+ {
+ ViewModel = catalog,
+ TabBarItem = new UITabBarItem("Covers", null, 2),
+ };
+
+ BookShelfViewController shelf = new()
+ {
+ ViewModel = catalog,
+ TabBarItem = new UITabBarItem("Shelf", null, 3),
+ };
+
+ ViewControllers = [catalogHost, membersNav, coverPager, shelf];
+
+ _ = this.WhenActivated(d =>
+ {
+ d(Activated.Subscribe(static _ => Console.WriteLine("LibraryTabBarController activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("LibraryTabBarController deactivated.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/LoanViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/LoanViewController.cs
new file mode 100644
index 0000000000..df43547ff7
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/LoanViewController.cs
@@ -0,0 +1,89 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The loan page: pick a member, then confirm or cancel. Pushed by .
+[System.Diagnostics.DebuggerDisplay("LoanViewController")]
+public sealed class LoanViewController : ReactiveViewController
+{
+ /// Names the book being loaned.
+ private readonly UILabel _bookLabel = new() { Font = UIFont.PreferredHeadline! };
+
+ /// Hosts one button per member.
+ private readonly UIStackView _memberButtons = new() { Axis = UILayoutConstraintAxis.Vertical, Spacing = 8, TranslatesAutoresizingMaskIntoConstraints = false };
+
+ /// Gets the label naming the picked member. Internal for the binding source generator.
+ internal UILabel SelectedMemberLabel { get; } = new() { TextColor = UIColor.SecondaryLabel };
+
+ /// Gets the button that confirms the loan. Internal so the binding source generator can observe it.
+ internal UIButton ConfirmButton { get; } = UIButton.FromType(UIButtonType.System);
+
+ /// Gets the button that cancels the loan. Internal so the binding source generator can observe it.
+ internal UIButton CancelButton { get; } = UIButton.FromType(UIButtonType.System);
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ Title = "Loan";
+ View!.BackgroundColor = UIColor.SystemBackground;
+ ConfirmButton.SetTitle("Confirm loan", UIControlState.Normal);
+ CancelButton.SetTitle("Cancel", UIControlState.Normal);
+
+ UIStackView layout = new([_bookLabel, _memberButtons, SelectedMemberLabel, ConfirmButton, CancelButton])
+ {
+ Axis = UILayoutConstraintAxis.Vertical,
+ Spacing = 12,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+ View!.AddSubview(layout);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ layout.LeadingAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.LeadingAnchor, 16),
+ layout.TrailingAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TrailingAnchor, -16),
+ layout.TopAnchor.ConstraintEqualTo(View!.SafeAreaLayoutGuide.TopAnchor, 16),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ _bookLabel.Text = $"Loaning: {ViewModel!.Book.Title}";
+
+ foreach (Member member in ViewModel.Members)
+ {
+ UIButton button = UIButton.FromType(UIButtonType.System);
+ button.SetTitle(member.Name, UIControlState.Normal);
+ button.TouchUpInside += (_, _) => ViewModel.SelectedMember = member;
+ _memberButtons.AddArrangedSubview(button);
+ }
+
+ d(this.OneWayBind(
+ ViewModel,
+ static vm => vm.SelectedMember,
+ static v => v.SelectedMemberLabel.Text,
+ static member => member is null ? "(no member selected)" : $"To: {member.Name}"));
+ d(this.BindCommand(ViewModel, static vm => vm.ConfirmLoan, static v => v.ConfirmButton));
+ d(this.BindCommand(ViewModel, static vm => vm.Cancel, static v => v.CancelButton));
+ });
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _bookLabel.Dispose();
+ _memberButtons.Dispose();
+ SelectedMemberLabel.Dispose();
+ ConfirmButton.Dispose();
+ CancelButton.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/MembersNavigationController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/MembersNavigationController.cs
new file mode 100644
index 0000000000..1dde721edd
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/MembersNavigationController.cs
@@ -0,0 +1,17 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// Wraps in a navigation bar, its own ViewModel set once.
+[System.Diagnostics.DebuggerDisplay("MembersNavigationController")]
+public sealed class MembersNavigationController : ReactiveNavigationController
+{
+ /// Initializes a new instance of the class.
+ /// The members data the root page shows.
+ public MembersNavigationController(MembersViewModel viewModel)
+ : base(new MembersPlaceholderViewController { ViewModel = viewModel }) =>
+ ViewModel = viewModel;
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/MembersPlaceholderViewController.cs b/src/examples/Documentation/Pages/platform-apple/iOS/MembersPlaceholderViewController.cs
new file mode 100644
index 0000000000..5d34a441c5
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/MembersPlaceholderViewController.cs
@@ -0,0 +1,83 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using CoreGraphics;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The members tab: a chip strip of every member above a table of the same members, one row each.
+[System.Diagnostics.DebuggerDisplay("MembersPlaceholderViewController")]
+public sealed class MembersPlaceholderViewController : ReactiveTableViewController
+{
+ /// Gets the horizontal chip strip above the table. Internal so the binding source generator can reach it.
+ internal MemberChipStripView ChipStrip { get; } = new(
+ new CGRect(0, 0, 320, 56),
+ new UICollectionViewFlowLayout
+ {
+ ScrollDirection = UICollectionViewScrollDirection.Horizontal,
+ ItemSize = new CGSize(48, 48),
+ MinimumInteritemSpacing = 12,
+ SectionInset = new UIEdgeInsets(8, 16, 8, 16),
+ });
+
+ /// Gets the reactive table source bound in .
+ internal ReactiveTableViewSource? Source { get; private set; }
+
+ ///
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+
+ Title = "Members";
+ ChipStrip.ViewModel = ViewModel;
+ TableView.RegisterClassForCellReuse(typeof(MemberCell), MemberCell.Key);
+ TableView.TableHeaderView = ChipStrip;
+
+ _ = this.WhenActivated(d =>
+ {
+ TableSectionInformation section = new(
+ ViewModel!.Members,
+ MemberCell.Key,
+ sizeHint: 56F,
+ static cell => Console.WriteLine($"Initializing a {cell.GetType().Name}."))
+ {
+ Header = new TableSectionHeader("Members"),
+ Footer = new TableSectionHeader(
+ static () => new UILabel
+ {
+ Text = "Tap a member to see their card.",
+ TextAlignment = UITextAlignment.Center,
+ Font = UIFont.PreferredFootnote!,
+ TextColor = UIColor.SecondaryLabel,
+ },
+ 24F),
+ };
+ IReadOnlyList> sections = [section];
+
+ d(Signal.Emit(sections).BindTo(TableView, source =>
+ {
+ Source = source;
+ return source.ElementSelected.Subscribe(static item => Console.WriteLine($"Selected member {((Member)item!).Name}."));
+ }));
+
+ TableSectionInformation readBack = Source!.Data[0];
+ Console.WriteLine(
+ $"{readBack.Header?.Title}: {Source.Data.Count} section(s), row height {readBack.SizeHint}, "
+ + $"has a collection {readBack.Collection is not null}, has an init action {readBack.InitializeCellAction is not null}.");
+ });
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ ChipStrip.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Program.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Program.cs
new file mode 100644
index 0000000000..aceaa41adc
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Program.cs
@@ -0,0 +1,17 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The iOS entry point.
+internal static class Program
+{
+ /// Hands control to UIKit, which creates and calls its lifecycle methods.
+ /// The process arguments UIKit forwards to UIApplicationMain.
+ private static void Main(string[] args) =>
+ UIApplication.Main(args, null, typeof(AppDelegate));
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverCell.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverCell.cs
new file mode 100644
index 0000000000..3705fffdbc
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverCell.cs
@@ -0,0 +1,63 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One cover tile in the book-cover grid: title and loan status.
+[System.Diagnostics.DebuggerDisplay("BookCoverCell")]
+public sealed class BookCoverCell : ReactiveCollectionViewCell
+{
+ /// Initializes a new instance of the class from a native handle.
+ /// The native handle UIKit hands the base constructor.
+ public BookCoverCell(IntPtr handle)
+ : base(handle)
+ {
+ ContentView.BackgroundColor = UIColor.SecondarySystemBackground;
+
+ UIStackView stack = new([TitleLabel, StatusLabel])
+ {
+ Axis = UILayoutConstraintAxis.Vertical,
+ Spacing = 2,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+ ContentView.AddSubview(stack);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ stack.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 8),
+ stack.TrailingAnchor.ConstraintEqualTo(ContentView.TrailingAnchor, -8),
+ stack.CenterYAnchor.ConstraintEqualTo(ContentView.CenterYAnchor),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.Text));
+ d(this.OneWayBind(
+ ViewModel,
+ static vm => vm.IsOnLoan,
+ static v => v.StatusLabel.Text,
+ static onLoan => onLoan ? "On loan" : "Available"));
+ });
+ }
+
+ /// Gets the book's title. Internal so the binding source generator can observe it.
+ internal UILabel TitleLabel { get; } = new() { Font = UIFont.PreferredCaption1!, Lines = 2 };
+
+ /// Gets the book's loan status. Internal so the binding source generator can observe it.
+ internal UILabel StatusLabel { get; } = new() { Font = UIFont.PreferredCaption2!, TextColor = UIColor.SecondaryLabel };
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ StatusLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverImageView.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverImageView.cs
new file mode 100644
index 0000000000..b38cbc61df
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookCoverImageView.cs
@@ -0,0 +1,26 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A book's cover. This app has no bundled artwork, so a blank image stands in and the accessibility label
+/// carries the book's title instead.
+[System.Diagnostics.DebuggerDisplay("BookCoverImageView")]
+public sealed class BookCoverImageView : ReactiveImageView
+{
+ /// Initializes a new instance of the class.
+ public BookCoverImageView()
+ {
+ Image ??= new UIImage();
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.AccessibilityLabel));
+ d(ThrownExceptions.Subscribe(static error => Console.WriteLine($"BookCoverImageView threw: {error.Message}")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookRowView.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookRowView.cs
new file mode 100644
index 0000000000..36824388ad
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/BookRowView.cs
@@ -0,0 +1,72 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using CoreGraphics;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One row in the catalog: title, author, and loan status. A real catalog would use a reactive table source instead.
+[System.Diagnostics.DebuggerDisplay("BookRowView")]
+public sealed class BookRowView : ReactiveView
+{
+ /// Initializes a new instance of the class.
+ public BookRowView() => Initialize();
+
+ /// Initializes a new instance of the class with an explicit frame.
+ /// The row's initial frame.
+ public BookRowView(CGRect frame)
+ : base(frame) =>
+ Initialize();
+
+ /// Gets the book's title. Internal so the binding source generator can observe it.
+ internal UILabel TitleLabel { get; } = new() { Font = UIFont.PreferredHeadline! };
+
+ /// Gets the book's loan status. Internal so the binding source generator can observe it.
+ internal UILabel StatusLabel { get; } = new() { Font = UIFont.PreferredSubheadline!, TextColor = UIColor.SecondaryLabel };
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ StatusLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ /// Lays out the labels and binds them to the book set as the view model before this row is added to a superview.
+ private void Initialize()
+ {
+ UIStackView stack = new([TitleLabel, StatusLabel])
+ {
+ Axis = UILayoutConstraintAxis.Vertical,
+ Spacing = 2,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+ AddSubview(stack);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ stack.LeadingAnchor.ConstraintEqualTo(LeadingAnchor),
+ stack.TrailingAnchor.ConstraintEqualTo(TrailingAnchor),
+ stack.TopAnchor.ConstraintEqualTo(TopAnchor),
+ stack.BottomAnchor.ConstraintEqualTo(BottomAnchor),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.Text));
+ d(this.OneWayBind(
+ ViewModel,
+ static vm => vm.IsOnLoan,
+ static v => v.StatusLabel.Text,
+ static onLoan => onLoan ? "On loan" : "Available"));
+ d(Activated.Subscribe(static _ => Console.WriteLine("BookRowView activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("BookRowView deactivated.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanBoardView.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanBoardView.cs
new file mode 100644
index 0000000000..ba35031727
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanBoardView.cs
@@ -0,0 +1,29 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
+using CoreGraphics;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A plain hosted directly as a subview, listing books currently on loan.
+[System.Diagnostics.DebuggerDisplay("LoanBoardView")]
+public sealed class LoanBoardView : ReactiveTableView
+{
+ /// Initializes a new instance of the class.
+ /// The board's initial frame.
+ public LoanBoardView(CGRect frame)
+ : base(frame, UITableViewStyle.Plain)
+ {
+ _ = this.WhenActivated(d =>
+ {
+ ObservableCollection onLoan = new(ViewModel!.Books.Where(static book => book.IsOnLoan));
+ d(Signal.Emit(onLoan).BindTo(this, sizeHint: 44F));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanedBookCell.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanedBookCell.cs
new file mode 100644
index 0000000000..c12e9f74de
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/LoanedBookCell.cs
@@ -0,0 +1,44 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One row in the on-loan board: a book's title.
+[System.Diagnostics.DebuggerDisplay("LoanedBookCell")]
+public sealed class LoanedBookCell : ReactiveTableViewCell
+{
+ /// Initializes a new instance of the class from a native handle.
+ /// The native handle UIKit hands the base constructor.
+ public LoanedBookCell(IntPtr handle)
+ : base(handle)
+ {
+ ContentView.AddSubview(TitleLabel);
+ TitleLabel.TranslatesAutoresizingMaskIntoConstraints = false;
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ TitleLabel.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 16),
+ TitleLabel.CenterYAnchor.ConstraintEqualTo(ContentView.CenterYAnchor),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.Text)));
+ }
+
+ /// Gets the book's title. Internal so the binding source generator can observe it.
+ internal UILabel TitleLabel { get; } = new() { Font = UIFont.PreferredBody! };
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberCell.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberCell.cs
new file mode 100644
index 0000000000..b22eaaaae1
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberCell.cs
@@ -0,0 +1,74 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using Foundation;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One row of the members table: a member's name, and the one cell that shows the six classic IReactiveObject members every reactive Apple type carries.
+[System.Diagnostics.DebuggerDisplay("MemberCell")]
+public sealed class MemberCell : ReactiveTableViewCell
+{
+ /// The reuse identifier registers this cell under.
+ internal static readonly NSString Key = new(nameof(MemberCell));
+
+ /// Initializes a new instance of the class from a native handle.
+ /// The native handle UIKit hands the base constructor.
+ public MemberCell(IntPtr handle)
+ : base(handle)
+ {
+ ContentView.AddSubview(NameLabel);
+ NameLabel.TranslatesAutoresizingMaskIntoConstraints = false;
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ NameLabel.LeadingAnchor.ConstraintEqualTo(ContentView.LeadingAnchor, 16),
+ NameLabel.CenterYAnchor.ConstraintEqualTo(ContentView.CenterYAnchor),
+ ]);
+
+ // The classic events fire for any property change; a cell can use them without going through Changed/Changing.
+ PropertyChanged += static (_, e) => Console.WriteLine($"MemberCell.{e.PropertyName} changed (classic event).");
+ PropertyChanging += static (_, e) => Console.WriteLine($"MemberCell.{e.PropertyName} changing (classic event).");
+
+ _ = this.WhenActivated(d =>
+ {
+ // Member is an immutable record, not a ReactiveObject, so the label follows ViewModel itself changing
+ // (which the cell base class does raise) rather than a OneWayBind on one of its properties.
+ d(this.WhenAnyValue(static v => v.ViewModel).Subscribe(member => NameLabel.Text = member?.Name));
+ d(Changing.Subscribe(static _ => Console.WriteLine("MemberCell changing.")));
+ d(Changed.Subscribe(static _ => Console.WriteLine("MemberCell changed.")));
+ d(ThrownExceptions.Subscribe(static error => Console.WriteLine($"MemberCell binding failed: {error.Message}")));
+ d(Activated.Subscribe(static _ => Console.WriteLine("MemberCell activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("MemberCell deactivated.")));
+ });
+ }
+
+ /// Gets the member's name. Internal so the binding source generator can observe it.
+ internal UILabel NameLabel { get; } = new() { Font = UIFont.PreferredBody! };
+
+ ///
+ public override void PrepareForReuse()
+ {
+ // Clearing the binding target before reuse should not itself look like a change to any observer, so it runs
+ // inside a suppression scope. Only the real value set once the cell is dequeued again should notify.
+ using (SuppressChangeNotifications())
+ {
+ ViewModel = null;
+ }
+
+ base.PrepareForReuse();
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ NameLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipCell.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipCell.cs
new file mode 100644
index 0000000000..c42a0129d1
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipCell.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using CoreGraphics;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One chip in the horizontal member strip: the member's initial in a circle.
+[System.Diagnostics.DebuggerDisplay("MemberChipCell")]
+public sealed class MemberChipCell : ReactiveCollectionViewCell
+{
+ /// Initializes a new instance of the class from a native handle.
+ /// The native handle UIKit hands the base constructor.
+ public MemberChipCell(IntPtr handle)
+ : base(handle)
+ {
+ ContentView.BackgroundColor = UIColor.SystemGray5;
+ ContentView.Layer.CornerRadius = 20;
+ ContentView.AddSubview(InitialLabel);
+ InitialLabel.Frame = new CGRect(0, 0, 40, 40);
+ InitialLabel.AutoresizingMask = UIViewAutoresizing.FlexibleDimensions;
+
+ // Member is an immutable record, not a ReactiveObject, so the label follows ViewModel itself changing rather
+ // than a OneWayBind on one of its properties.
+ _ = this.WhenActivated(d =>
+ d(this.WhenAnyValue(static v => v.ViewModel)
+ .Subscribe(member => InitialLabel.Text = member?.Name[..1].ToUpperInvariant())));
+ }
+
+ /// Gets the member's initial. Internal so the binding source generator can observe it.
+ internal UILabel InitialLabel { get; } = new() { TextAlignment = UITextAlignment.Center, Font = UIFont.PreferredHeadline! };
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ InitialLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipStripView.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipStripView.cs
new file mode 100644
index 0000000000..1e87bc26ee
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/MemberChipStripView.cs
@@ -0,0 +1,29 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Collections.Specialized;
+using CoreGraphics;
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A plain : no collection view controller owns it, so
+/// hosts it directly as the members table's header, listing the same
+/// members as a horizontal strip of chips.
+[System.Diagnostics.DebuggerDisplay("MemberChipStripView")]
+public sealed class MemberChipStripView : ReactiveCollectionView
+{
+ /// Initializes a new instance of the class.
+ /// The strip's initial frame.
+ /// The horizontal flow layout the strip scrolls under.
+ public MemberChipStripView(CGRect frame, UICollectionViewLayout layout)
+ : base(frame, layout)
+ {
+ BackgroundColor = UIColor.SystemBackground;
+
+ _ = this.WhenActivated(d =>
+ d(Signal.Emit(ViewModel!.Members).BindTo(this)));
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/ShelfHeaderView.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/ShelfHeaderView.cs
new file mode 100644
index 0000000000..c9e948e51b
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/ShelfHeaderView.cs
@@ -0,0 +1,47 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The single section header above the book-cover grid, naming how many books it shows.
+[System.Diagnostics.DebuggerDisplay("ShelfHeaderView")]
+public sealed class ShelfHeaderView : ReactiveCollectionReusableView
+{
+ /// Initializes a new instance of the class from a native handle.
+ /// The native handle UIKit hands the base constructor.
+ public ShelfHeaderView(IntPtr handle)
+ : base(handle)
+ {
+ AddSubview(CountLabel);
+ CountLabel.TranslatesAutoresizingMaskIntoConstraints = false;
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ CountLabel.LeadingAnchor.ConstraintEqualTo(LeadingAnchor, 16),
+ CountLabel.CenterYAnchor.ConstraintEqualTo(CenterYAnchor),
+ ]);
+
+ // A supplementary view is dequeued and given its ViewModel before UIKit adds it to the view hierarchy, so it
+ // reacts to the property directly rather than through WhenActivated.
+ _ = this.WhenAnyValue(static v => v.ViewModel)
+ .WhereNotNull()
+ .Subscribe(vm => CountLabel.Text = $"{vm.Books.Count} book(s)");
+ }
+
+ /// Gets the label naming how many books the shelf has. Internal so the binding source generator can observe it.
+ internal UILabel CountLabel { get; } = new() { Font = UIFont.PreferredHeadline! };
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ CountLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/iOS/Views/StarRatingControl.cs b/src/examples/Documentation/Pages/platform-apple/iOS/Views/StarRatingControl.cs
new file mode 100644
index 0000000000..46c56e3423
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/iOS/Views/StarRatingControl.cs
@@ -0,0 +1,73 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using UIKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A star-rating control wrapping a that binds two-way to .
+[System.Diagnostics.DebuggerDisplay("StarRatingControl {Stars}")]
+public sealed class StarRatingControl : ReactiveControl
+{
+ /// The stepper this control wraps. UIKit has no built-in star control, so a stepper stands in for one.
+ private readonly UIStepper _stepper = new() { MinimumValue = 0, MaximumValue = 5, TranslatesAutoresizingMaskIntoConstraints = false };
+
+ /// Initializes a new instance of the class.
+ public StarRatingControl() => Initialize();
+
+ /// Raised whenever changes, so the binding layer's naming convention (a
+ /// StarsChanged event for a Stars property) can drive a two-way Bind.
+ public event EventHandler? StarsChanged;
+
+ /// Gets or sets the rating, from 0 to 5.
+ public int Stars
+ {
+ get => (int)_stepper.Value;
+ set => _stepper.Value = value;
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _stepper.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ /// Builds the stepper, wires its native event to , and binds it to .
+ private void Initialize()
+ {
+ AddSubview(_stepper);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ _stepper.LeadingAnchor.ConstraintEqualTo(LeadingAnchor),
+ _stepper.TopAnchor.ConstraintEqualTo(TopAnchor),
+ _stepper.BottomAnchor.ConstraintEqualTo(BottomAnchor),
+ ]);
+ _stepper.ValueChanged += (_, _) => StarsChanged?.Invoke(this, EventArgs.Empty);
+
+ // A rating of 0 never fires a change notification on its own, so start silent rather than log a phantom change.
+ using (SuppressChangeNotifications())
+ {
+ Stars = 0;
+ }
+
+ PropertyChanging += static (_, e) => Console.WriteLine($"StarRatingControl.{e.PropertyName} is changing.");
+ PropertyChanged += (_, e) => Console.WriteLine($"StarRatingControl.{e.PropertyName} changed to {Stars}.");
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.Bind(ViewModel, static vm => vm.Rating, static v => v.Stars));
+ d(Changed.Subscribe(static change => Console.WriteLine($"StarRatingControl.{change.PropertyName} changed (Changed stream).")));
+ d(Changing.Subscribe(static change => Console.WriteLine($"StarRatingControl.{change.PropertyName} is changing (Changing stream).")));
+ d(ThrownExceptions.Subscribe(static error => Console.WriteLine($"StarRatingControl threw: {error.Message}")));
+ d(Activated.Subscribe(static _ => Console.WriteLine("StarRatingControl activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("StarRatingControl deactivated.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/AppDelegate.cs b/src/examples/Documentation/Pages/platform-apple/macOS/AppDelegate.cs
new file mode 100644
index 0000000000..748fa9e877
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/AppDelegate.cs
@@ -0,0 +1,79 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+using Foundation;
+using ReactiveUI.Builder;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The app's NSApplicationDelegate: builds ReactiveUI, wires up state suspension, and shows the window.
+[Register(nameof(AppDelegate))]
+[System.Diagnostics.DebuggerDisplay("AppDelegate")]
+public sealed class AppDelegate : NSApplicationDelegate
+{
+ /// Keeps AppKit lifecycle notifications translated into suspend/resume signals for as long as the process lives.
+ private AutoSuspendHelper? _autoSuspendHelper;
+
+ /// Keeps the window controller alive for as long as the app runs.
+ private MainWindowController? _mainWindowController;
+
+ ///
+ public override void DidFinishLaunching(NSNotification notification)
+ {
+ ReactiveUIBuilder builder = RxAppBuilder.CreateReactiveUIBuilder();
+ _ = builder.WithPlatformModule().BuildApp();
+
+ _autoSuspendHelper = new AutoSuspendHelper(this);
+ _autoSuspendHelper.DidFinishLaunching(notification);
+ RxSuspension.SuspensionHost.CreateNewAppState = static () => new LibraryAppState();
+
+ AppSupportJsonSuspensionDriver driver = new();
+ RxSuspension.SuspensionHost.SetupDefaultSuspendResume(driver);
+
+ _ = driver.SaveState(new LibraryAppState { LastViewedBook = "Dune" }, LibraryAppStateJsonContext.Default.LibraryAppState)
+ .Subscribe(static _ => Console.WriteLine("AppSupportJsonSuspensionDriver saved the app state."));
+ _ = driver.LoadState(LibraryAppStateJsonContext.Default.LibraryAppState)
+ .Subscribe(
+ static state => Console.WriteLine($"AppSupportJsonSuspensionDriver loaded state for {state?.LastViewedBook}."),
+ static error => Console.WriteLine($"AppSupportJsonSuspensionDriver had nothing to load yet: {error.Message}."));
+ _ = driver.InvalidateState().Subscribe(static _ => Console.WriteLine("AppSupportJsonSuspensionDriver invalidated the saved state."));
+
+ PlatformOperations platformOperations = new();
+ string? orientation = platformOperations.GetOrientation();
+ Console.WriteLine($"Device orientation reported by AppKit: {orientation ?? "(none; orientation is a UIKit concept)"}.");
+
+ _mainWindowController = new MainWindowController();
+ _mainWindowController.ShowWindow(this);
+ }
+
+ ///
+ public override void DidBecomeActive(NSNotification notification) =>
+ _autoSuspendHelper?.DidBecomeActive(notification);
+
+ ///
+ public override void DidResignActive(NSNotification notification) =>
+ _autoSuspendHelper?.DidResignActive(notification);
+
+ ///
+ public override void DidHide(NSNotification notification) =>
+ _autoSuspendHelper?.DidHide(notification);
+
+ ///
+ public override NSApplicationTerminateReply ApplicationShouldTerminate(NSApplication sender) =>
+ _autoSuspendHelper?.ApplicationShouldTerminate(sender) ?? NSApplicationTerminateReply.Now;
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _autoSuspendHelper?.Dispose();
+ _mainWindowController?.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/BookDetailViewController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/BookDetailViewController.cs
new file mode 100644
index 0000000000..c7d327392e
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/BookDetailViewController.cs
@@ -0,0 +1,78 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One book's detail: cover, title, author, and a star rating. Hosted as the split view's detail pane.
+[System.Diagnostics.DebuggerDisplay("BookDetailViewController")]
+public sealed class BookDetailViewController : ReactiveViewController
+{
+ /// Hosts the cover and rating controls, added once the view model is set on them first.
+ private NSStackView _layout = new();
+
+ /// Gets the label showing the book's title. Internal so the binding source generator can observe it.
+ internal NSTextField TitleLabel { get; } = NSTextField.CreateLabel(string.Empty);
+
+ /// Gets the label showing the book's author. Internal so the binding source generator can observe it.
+ internal NSTextField AuthorLabel { get; } = NSTextField.CreateLabel(string.Empty);
+
+ ///
+ public override void LoadView()
+ {
+ NSView view = new();
+ View = view;
+
+ _layout = new NSStackView { Orientation = NSUserInterfaceLayoutOrientation.Vertical, Spacing = 12, TranslatesAutoresizingMaskIntoConstraints = false };
+ _layout.AddArrangedSubview(TitleLabel);
+ _layout.AddArrangedSubview(AuthorLabel);
+ view.AddSubview(_layout);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ _layout.CenterXAnchor.ConstraintEqualTo(view.CenterXAnchor),
+ _layout.TopAnchor.ConstraintEqualTo(view.TopAnchor, 24),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ // Setting ViewModel before adding each control to the stack means it already has a book to bind to the
+ // moment it gains a superview and its own activation runs.
+ BookCoverImageView cover = new() { ViewModel = ViewModel, TranslatesAutoresizingMaskIntoConstraints = false };
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ cover.WidthAnchor.ConstraintEqualTo(120),
+ cover.HeightAnchor.ConstraintEqualTo(160),
+ ]);
+ _layout.InsertArrangedSubview(cover, 0);
+
+ StarRatingControl rating = new() { ViewModel = ViewModel };
+ _layout.AddArrangedSubview(rating);
+
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.StringValue));
+ d(this.OneWayBind(ViewModel, static vm => vm.Author, static v => v.AuthorLabel.StringValue));
+ d(new ActionDisposable(() =>
+ {
+ cover.RemoveFromSuperview();
+ cover.Dispose();
+ rating.RemoveFromSuperview();
+ rating.Dispose();
+ }));
+ });
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ AuthorLabel.Dispose();
+ _layout.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/BookListViewController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/BookListViewController.cs
new file mode 100644
index 0000000000..b67be5cb0e
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/BookListViewController.cs
@@ -0,0 +1,67 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+using CoreGraphics;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The catalog page: one row per book with a "Select" button, and a button that loans the selected book.
+[System.Diagnostics.DebuggerDisplay("BookListViewController")]
+public sealed class BookListViewController : ReactiveViewController
+{
+ /// The stack that hosts one row per book. A real catalog would use a reactive table source instead.
+ private readonly NSStackView _rows = new() { Orientation = NSUserInterfaceLayoutOrientation.Vertical, Spacing = 8, TranslatesAutoresizingMaskIntoConstraints = false };
+
+ /// Gets the button that opens the loan page for the selected book. Internal for the binding source generator.
+ internal NSButton LoanButton { get; } = new() { Title = "Loan selected book", BezelStyle = NSBezelStyle.Rounded };
+
+ ///
+ public override void LoadView()
+ {
+ NSView view = new();
+ View = view;
+
+ NSStackView layout = new() { Orientation = NSUserInterfaceLayoutOrientation.Vertical, Spacing = 16, TranslatesAutoresizingMaskIntoConstraints = false };
+ layout.AddArrangedSubview(_rows);
+ layout.AddArrangedSubview(LoanButton);
+ view.AddSubview(layout);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ layout.LeadingAnchor.ConstraintEqualTo(view.LeadingAnchor, 16),
+ layout.TopAnchor.ConstraintEqualTo(view.TopAnchor, 16),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ foreach (Book book in ViewModel!.Books)
+ {
+ // The frame constructor pre-sizes the row; Auto Layout resizes it once ViewModel bindings run.
+ BookRowView row = new(CGRect.Empty) { ViewModel = book };
+ NSButton selectButton = new() { Title = $"Select {book.Title}", BezelStyle = NSBezelStyle.Rounded };
+ selectButton.Activated += (_, _) => ViewModel!.SelectedBook = book;
+
+ NSStackView rowLayout = new() { Orientation = NSUserInterfaceLayoutOrientation.Horizontal, Spacing = 8 };
+ rowLayout.AddArrangedSubview(row);
+ rowLayout.AddArrangedSubview(selectButton);
+ _rows.AddArrangedSubview(rowLayout);
+ }
+
+ d(this.BindCommand(ViewModel, static vm => vm.OpenLoan, static v => v.LoanButton));
+ });
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _rows.Dispose();
+ LoanButton.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/LibrarySplitViewController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/LibrarySplitViewController.cs
new file mode 100644
index 0000000000..ce4f964c3b
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/LibrarySplitViewController.cs
@@ -0,0 +1,52 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The window's content: the catalog as one split item, a as the other.
+[System.Diagnostics.DebuggerDisplay("LibrarySplitViewController")]
+public sealed class LibrarySplitViewController : ReactiveSplitViewController
+{
+ /// Shows the detail view for whichever book the catalog selects.
+ private readonly ViewModelViewHost _detailHost;
+
+ /// Initializes a new instance of the class.
+ /// The shell view model.
+ /// The catalog shown in the first split item.
+ public LibrarySplitViewController(LibraryShellViewModel shell, BookCatalogViewModel catalog)
+ {
+ ViewModel = shell;
+
+ DefaultViewLocator detailLocator = new();
+ detailLocator.Map();
+ _detailHost = new ViewModelViewHost
+ {
+ DefaultContent = new NSViewController(),
+ ViewLocator = detailLocator,
+ };
+
+ BookListViewController master = new() { ViewModel = catalog };
+ AddSplitViewItem(NSSplitViewItem.FromViewController(master));
+ AddSplitViewItem(NSSplitViewItem.FromViewController(_detailHost));
+
+ _ = this.WhenActivated(d =>
+ d(catalog.WhenAnyValue(static vm => vm.SelectedBook)
+ .WhereNotNull()
+ .Subscribe(book => _detailHost.ViewModel = book)));
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _detailHost.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/MainWindowController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/MainWindowController.cs
new file mode 100644
index 0000000000..14cf421cd6
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/MainWindowController.cs
@@ -0,0 +1,62 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+using CoreGraphics;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+///
+/// The app's single window, hosting the library split view. It derives from
+/// , the form of the non-generic
+/// , so it has a ViewModel and can use WhenActivated.
+///
+[System.Diagnostics.DebuggerDisplay("MainWindowController")]
+public sealed class MainWindowController : ReactiveWindowController
+{
+ /// Initializes a new instance of the class with the library's sample data.
+ public MainWindowController()
+ : base(CreateWindow())
+ {
+ }
+
+ ///
+ public override void WindowDidLoad()
+ {
+ base.WindowDidLoad();
+
+ Member ada = new("M-1", "Ada");
+ Member grace = new("M-2", "Grace");
+ List members = [ada, grace];
+ List books =
+ [
+ new("Pride and Prejudice", "Jane Austen"),
+ new("The Hobbit", "J. R. R. Tolkien"),
+ new("Dune", "Frank Herbert"),
+ ];
+
+ LibraryShellViewModel shell = new();
+ BookCatalogViewModel catalog = new(shell, books, members);
+ ViewModel = shell;
+
+ Window!.ContentViewController = new LibrarySplitViewController(shell, catalog);
+
+ // ViewModel is set above, so the binding below has a shell to read Title from the moment activation runs.
+ _ = this.WhenActivated(d =>
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.Window!.Title)));
+ }
+
+ /// Creates the window this controller manages. AppKit needs the window before runs.
+ /// A borderless-menu, resizable window sized for the catalog.
+ private static NSWindow CreateWindow() =>
+ new(
+ new CGRect(0, 0, 640, 480),
+ NSWindowStyle.Titled | NSWindowStyle.Closable | NSWindowStyle.Miniaturizable | NSWindowStyle.Resizable,
+ NSBackingStore.Buffered,
+ false)
+ {
+ Title = "Library",
+ };
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/MembersPlaceholderViewController.cs b/src/examples/Documentation/Pages/platform-apple/macOS/MembersPlaceholderViewController.cs
new file mode 100644
index 0000000000..2400a0507e
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/MembersPlaceholderViewController.cs
@@ -0,0 +1,44 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The members placeholder: counts members. Listing each in a reactive table is a different chunk of the surface.
+[System.Diagnostics.DebuggerDisplay("MembersPlaceholderViewController")]
+public sealed class MembersPlaceholderViewController : ReactiveViewController
+{
+ /// Gets the label naming how many members the library has. Internal for the binding source generator.
+ internal NSTextField CountLabel { get; } = NSTextField.CreateLabel(string.Empty);
+
+ ///
+ public override void LoadView()
+ {
+ NSView view = new();
+ View = view;
+ view.AddSubview(CountLabel);
+ CountLabel.TranslatesAutoresizingMaskIntoConstraints = false;
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ CountLabel.CenterXAnchor.ConstraintEqualTo(view.CenterXAnchor),
+ CountLabel.TopAnchor.ConstraintEqualTo(view.TopAnchor, 24),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ d(this.OneWayBind(ViewModel, static vm => vm.MemberCount, static v => v.CountLabel.StringValue, static count => $"{count} member(s)")));
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ CountLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/Program.cs b/src/examples/Documentation/Pages/platform-apple/macOS/Program.cs
new file mode 100644
index 0000000000..e9dba423d8
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/Program.cs
@@ -0,0 +1,25 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// The macOS entry point.
+internal static class Program
+{
+ ///
+ /// Starts AppKit directly instead of calling NSApplication.Main, because this app builds its window and
+ /// menu in code and has no storyboard to wire up as NSApplication.Delegate.
+ ///
+ /// The process arguments. Unused: this app takes no command-line configuration.
+ private static void Main(string[] args)
+ {
+ _ = args;
+ NSApplication.Init();
+ NSApplication.SharedApplication.Delegate = new AppDelegate();
+ NSApplication.Main([]);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookCoverImageView.cs b/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookCoverImageView.cs
new file mode 100644
index 0000000000..ce8ec972e5
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookCoverImageView.cs
@@ -0,0 +1,25 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A book's cover. This app has no bundled artwork, so a blank image stands in and the tooltip carries the book's title.
+[System.Diagnostics.DebuggerDisplay("BookCoverImageView")]
+public sealed class BookCoverImageView : ReactiveImageView
+{
+ /// Initializes a new instance of the class.
+ public BookCoverImageView()
+ {
+ Image ??= new NSImage();
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.ToolTip));
+ d(ThrownExceptions.Subscribe(static error => Console.WriteLine($"BookCoverImageView threw: {error.Message}")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookRowView.cs b/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookRowView.cs
new file mode 100644
index 0000000000..c374b31be8
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/Views/BookRowView.cs
@@ -0,0 +1,74 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+using CoreGraphics;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// One row in the catalog: title, author, and loan status. A real catalog would use a reactive table source instead.
+[System.Diagnostics.DebuggerDisplay("BookRowView")]
+public sealed class BookRowView : ReactiveView
+{
+ /// Initializes a new instance of the class.
+ public BookRowView() => Initialize();
+
+ /// Initializes a new instance of the class with an explicit frame.
+ /// The row's initial frame.
+ public BookRowView(CGRect frame)
+ : base(frame) =>
+ Initialize();
+
+ /// Gets the book's title label. Internal so the binding source generator can observe it.
+ internal NSTextField TitleLabel { get; } = NSTextField.CreateLabel(string.Empty);
+
+ /// Gets the book's loan status label. Internal so the binding source generator can observe it.
+ internal NSTextField StatusLabel { get; } = NSTextField.CreateLabel(string.Empty);
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ TitleLabel.Dispose();
+ StatusLabel.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ /// Lays out the labels and binds them to the book set as the view model before this row is added to a superview.
+ private void Initialize()
+ {
+ NSStackView stack = new()
+ {
+ Orientation = NSUserInterfaceLayoutOrientation.Vertical,
+ Spacing = 2,
+ TranslatesAutoresizingMaskIntoConstraints = false,
+ };
+ stack.AddArrangedSubview(TitleLabel);
+ stack.AddArrangedSubview(StatusLabel);
+ AddSubview(stack);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ stack.LeadingAnchor.ConstraintEqualTo(LeadingAnchor),
+ stack.TrailingAnchor.ConstraintEqualTo(TrailingAnchor),
+ stack.TopAnchor.ConstraintEqualTo(TopAnchor),
+ stack.BottomAnchor.ConstraintEqualTo(BottomAnchor),
+ ]);
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.OneWayBind(ViewModel, static vm => vm.Title, static v => v.TitleLabel.StringValue));
+ d(this.OneWayBind(
+ ViewModel,
+ static vm => vm.IsOnLoan,
+ static v => v.StatusLabel.StringValue,
+ static onLoan => onLoan ? "On loan" : "Available"));
+ d(Activated.Subscribe(static _ => Console.WriteLine("BookRowView activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("BookRowView deactivated.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/macOS/Views/StarRatingControl.cs b/src/examples/Documentation/Pages/platform-apple/macOS/Views/StarRatingControl.cs
new file mode 100644
index 0000000000..c79c95d633
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/macOS/Views/StarRatingControl.cs
@@ -0,0 +1,73 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using AppKit;
+
+namespace ReactiveUI.Documentation.PlatformApple;
+
+/// A star-rating control wrapping an that binds two-way to .
+[System.Diagnostics.DebuggerDisplay("StarRatingControl {Stars}")]
+public sealed class StarRatingControl : ReactiveControl
+{
+ /// The stepper this control wraps. AppKit has no built-in star control, so a stepper stands in for one.
+ private readonly NSStepper _stepper = new() { MinValue = 0, MaxValue = 5, TranslatesAutoresizingMaskIntoConstraints = false };
+
+ /// Initializes a new instance of the class.
+ public StarRatingControl() => Initialize();
+
+ /// Raised whenever changes, so the binding layer's naming convention (a
+ /// StarsChanged event for a Stars property) can drive a two-way Bind.
+ public event EventHandler? StarsChanged;
+
+ /// Gets or sets the rating, from 0 to 5.
+ public int Stars
+ {
+ get => (int)_stepper.DoubleValue;
+ set => _stepper.DoubleValue = value;
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _stepper.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ /// Builds the stepper, wires its action to , and binds it to .
+ private void Initialize()
+ {
+ AddSubview(_stepper);
+ NSLayoutConstraint.ActivateConstraints(
+ [
+ _stepper.LeadingAnchor.ConstraintEqualTo(LeadingAnchor),
+ _stepper.TopAnchor.ConstraintEqualTo(TopAnchor),
+ _stepper.BottomAnchor.ConstraintEqualTo(BottomAnchor),
+ ]);
+ _stepper.Activated += (_, _) => StarsChanged?.Invoke(this, EventArgs.Empty);
+
+ // A rating of 0 never fires a change notification on its own, so start silent rather than log a phantom change.
+ using (SuppressChangeNotifications())
+ {
+ Stars = 0;
+ }
+
+ PropertyChanging += static (_, e) => Console.WriteLine($"StarRatingControl.{e.PropertyName} is changing.");
+ PropertyChanged += (_, e) => Console.WriteLine($"StarRatingControl.{e.PropertyName} changed to {Stars}.");
+
+ _ = this.WhenActivated(d =>
+ {
+ d(this.Bind(ViewModel, static vm => vm.Rating, static v => v.Stars));
+ d(Changed.Subscribe(static change => Console.WriteLine($"StarRatingControl.{change.PropertyName} changed (Changed stream).")));
+ d(Changing.Subscribe(static change => Console.WriteLine($"StarRatingControl.{change.PropertyName} is changing (Changing stream).")));
+ d(ThrownExceptions.Subscribe(static error => Console.WriteLine($"StarRatingControl threw: {error.Message}")));
+ d(Activated.Subscribe(static _ => Console.WriteLine("StarRatingControl activated.")));
+ d(Deactivated.Subscribe(static _ => Console.WriteLine("StarRatingControl deactivated.")));
+ });
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-apple/platform-apple.csproj b/src/examples/Documentation/Pages/platform-apple/platform-apple.csproj
new file mode 100644
index 0000000000..883dc97699
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-apple/platform-apple.csproj
@@ -0,0 +1,66 @@
+
+
+
+
+ net10.0-ios;net10.0-macos
+
+ net.reactiveui.documentation.platformapple
+ ReactiveUI Apple Examples
+ 1
+ 1.0
+
+ false
+
+ false
+ false
+ false
+
+
+
+ 15.0
+
+ iossimulator-arm64
+ iossimulator-x64
+ None
+
+
+
+ 12.0
+
+ osx-arm64
+ osx-x64
+ None
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/examples/Documentation/Pages/platform-blend-drawing/SmokeTest.cs b/src/examples/Documentation/Pages/platform-blend-drawing/SmokeTest.cs
index e423966f16..1bc3488ec3 100644
--- a/src/examples/Documentation/Pages/platform-blend-drawing/SmokeTest.cs
+++ b/src/examples/Documentation/Pages/platform-blend-drawing/SmokeTest.cs
@@ -52,7 +52,14 @@ public static void Run()
private static void ShowSchedulerOverrideForTesting()
{
Border probeElement = new();
+ Border targetElement = new();
FollowObservableStateBehavior stateBehavior = new() { SchedulerOverride = Sequencer.Immediate };
+
+ // A test that builds the behavior in code, rather than XAML, sets TargetObject through its dependency
+ // property field with SetValue; the window's XAML sets the same property with a binding instead.
+ stateBehavior.SetValue(FollowObservableStateBehavior.TargetObjectProperty, targetElement);
+ Console.WriteLine($"TargetObject via SetValue: {stateBehavior.GetValue(FollowObservableStateBehavior.TargetObjectProperty) == targetElement}");
+
stateBehavior.Attach(probeElement);
stateBehavior.StateObservable = Signal.Emit("Stormy");
Console.WriteLine("FollowObservableStateBehavior.SchedulerOverride delivered without a dispatcher pump.");
diff --git a/src/examples/Documentation/Pages/platform-maui/LoggingRecipeViewHost.cs b/src/examples/Documentation/Pages/platform-maui/LoggingRecipeViewHost.cs
new file mode 100644
index 0000000000..438cc26e08
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-maui/LoggingRecipeViewHost.cs
@@ -0,0 +1,24 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using ReactiveUI.Maui;
+
+namespace ReactiveUI.Documentation.PlatformMaui;
+
+/// A for that counts each resolution.
+[DebuggerDisplay("LoggingRecipeViewHost: {ResolutionCount} resolutions")]
+public sealed class LoggingRecipeViewHost : ViewModelViewHost
+{
+ /// Gets the number of times this host has resolved a view.
+ public int ResolutionCount { get; private set; }
+
+ ///
+ protected override void ResolveViewForViewModel(object? viewModel, string? contract)
+ {
+ ResolutionCount++;
+ base.ResolveViewForViewModel(viewModel, contract);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-maui/LoggingViewModelViewHost.cs b/src/examples/Documentation/Pages/platform-maui/LoggingViewModelViewHost.cs
new file mode 100644
index 0000000000..8e0137d16a
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-maui/LoggingViewModelViewHost.cs
@@ -0,0 +1,28 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using ReactiveUI.Maui;
+
+namespace ReactiveUI.Documentation.PlatformMaui;
+
+/// A that records the view model type it resolved a view for each time.
+[DebuggerDisplay("LoggingViewModelViewHost: {ResolvedViewModelTypes.Count} resolved")]
+public sealed class LoggingViewModelViewHost : ViewModelViewHost
+{
+ /// Gets the view model type names resolved so far, in order.
+ public List ResolvedViewModelTypes { get; } = [];
+
+ ///
+ protected override void ResolveViewForViewModel(object? viewModel, string? contract)
+ {
+ if (viewModel is not null)
+ {
+ ResolvedViewModelTypes.Add(viewModel.GetType().Name);
+ }
+
+ base.ResolveViewForViewModel(viewModel, contract);
+ }
+}
diff --git a/src/examples/Documentation/Pages/platform-maui/ViewModelViewHostExamples.cs b/src/examples/Documentation/Pages/platform-maui/ViewModelViewHostExamples.cs
index 06b9133ad7..7b1c9eee95 100644
--- a/src/examples/Documentation/Pages/platform-maui/ViewModelViewHostExamples.cs
+++ b/src/examples/Documentation/Pages/platform-maui/ViewModelViewHostExamples.cs
@@ -159,4 +159,34 @@ public static void GenericHostTypesTheViewModelProperty()
// 3
// True
}
+
+ /// Overriding ResolveViewForViewModel lets a host observe each resolution before the base class runs it.
+ public static void OverridingResolveViewForViewModelObservesEachResolution()
+ {
+ AppLocator.CurrentMutable.Register>(static () => new RecipeListContentView());
+
+ LoggingViewModelViewHost host = new();
+ host.ViewModel = new RecipeListViewModel(new RecipeBookScreen());
+ host.ViewModel = null;
+
+ Console.WriteLine(string.Join(", ", host.ResolvedViewModelTypes));
+
+ // Output:
+ // RecipeListViewModel
+ }
+
+ /// overrides the same method, so a generic host can observe resolutions too.
+ public static void GenericHostOverridesResolveViewForViewModelToo()
+ {
+ AppLocator.CurrentMutable.Register>(static () => new RecipeListContentView());
+
+ LoggingRecipeViewHost host = new();
+ host.ViewModel = new RecipeListViewModel(new RecipeBookScreen());
+ host.ViewModel = new RecipeListViewModel(new RecipeBookScreen());
+
+ Console.WriteLine(host.ResolutionCount);
+
+ // Output:
+ // 2
+ }
}
diff --git a/src/examples/Documentation/Pages/platform-maui/VisibilityConverterExamples.cs b/src/examples/Documentation/Pages/platform-maui/VisibilityConverterExamples.cs
index 07696ab95a..ecbc3278cb 100644
--- a/src/examples/Documentation/Pages/platform-maui/VisibilityConverterExamples.cs
+++ b/src/examples/Documentation/Pages/platform-maui/VisibilityConverterExamples.cs
@@ -55,4 +55,18 @@ public static void VisibilityToBooleanConvertsBack()
// False
// False
}
+
+ /// GetAffinityForObjects reports how well each converter matches a binding; ReactiveUI's binding resolution calls it to pick a converter.
+ public static void GetAffinityForObjectsReportsTheBuiltInScore()
+ {
+ BooleanToVisibilityTypeConverter toVisibility = new();
+ VisibilityToBooleanTypeConverter toBoolean = new();
+
+ Console.WriteLine(toVisibility.GetAffinityForObjects());
+ Console.WriteLine(toBoolean.GetAffinityForObjects());
+
+ // Output:
+ // 2
+ // 2
+ }
}
diff --git a/src/examples/Documentation/Pages/platform-winforms/ContentSetMethodConvertersExamples.cs b/src/examples/Documentation/Pages/platform-winforms/ContentSetMethodConvertersExamples.cs
new file mode 100644
index 0000000000..a21d4b9204
--- /dev/null
+++ b/src/examples/Documentation/Pages/platform-winforms/ContentSetMethodConvertersExamples.cs
@@ -0,0 +1,77 @@
+// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using ReactiveUI.Winforms;
+
+namespace ReactiveUI.Documentation.PlatformWinforms;
+
+///
+/// Shows and , the
+/// set-method converters Bind uses to fill a 's or 's
+/// Controls collection from a source, since neither collection has a normal property setter.
+///
+public static class ContentSetMethodConvertersExamples
+{
+ /// The panel converter matches a source that holds s and a target of ; nothing else.
+ public static void PanelConverterMatchesAControlCollection()
+ {
+ PanelSetMethodBindingConverter converter = new();
+
+ Console.WriteLine(converter.GetAffinityForObjects(typeof(List