1 Rock Mobile in-app appearance setting: Light Mode and the superior Dark Mode Shared by Yeşu Chum, Houston's First Baptist Church 16 hours ago 19.6 Mobile Intermediate Rock Mobile Appearance Setting Rock Mobile follows the phone's light or dark system setting, and Rock doesn't have a setting to change that inside the app. This recipe adds a System / Light / Dark option to your app using page XAML, Lava and Rock's own commands so the choice can persist. What Does This Thing Do? It adds an Appearance card to a page in your app with three options, and the app uses whichever one the person picks instead of just the system settings. System follows the phone, Light and Dark to override it Save the cahge right away. Your pages, Rock's own blocks and form fields, and the nav and tab bars all change following your style settings. The choice is saved on the phone and applied again when the app opens It's two Content blocks, easy peasy. Before You Start I built and tested this on Rock 19.7 with the Blended style framework, on Rock Mobile shells v16 (App Store) through 19.6 (TestFlight). It may work on earlier versions, but test it out first. The card uses Rock's interface color classes, which come with Blended and Standard; on Legacy, swap in your own classes. Set it up on a staff-only test page first. One of the blocks ends up on your app's start page, and if it's set up wrong there the start page shows an error for everyone. The Setup in 6 Steps Work through these in order. Steps 1 through 5 happen on a test page, and step 6 moves the blocks to where they'll live. All the code is in this recipe, and the card is also attached as AppearanceCard.xaml if you'd rather download it. If you want to know how it works, that's covered after the setup. Step 1 - Make a Staff-Only Test Page Go to Admin Tools > CMS Configuration > Mobile Applications and open your app. On the Pages tab, add a page named Theme Test and leave Display In Navigation unchecked. Copy the new page's Guid from its settings. On a page you already visit in the app, add this link to its Content block so only you can see it. Swap in your Person Id and the Guid from the last step. {%- if CurrentPerson.Id == 1234 -%} <Button Text="Theme Test" StyleClass="btn, btn-link" Command="{Binding PushPage}" CommandParameter="your-test-page-guid" /> {%- endif -%} The block you add the link to needs Dynamic Content on, so CurrentPerson is available. Step 2 - Add the Appearance Card On the Theme Test page, add a Content block named Appearance. Paste this into its Content: {%- assign accentColor = '#2563EB' -%} {%- assign themeKey = 'ThemeIndex' -%} {%- assign themeOptions = 'Unspecified|System|adjust,Light|Light|sun,Dark|Dark|moon' | Split:',' -%} <VerticalStackLayout Spacing="8"> <Label Text="APPEARANCE" StyleClass="footnote, bold, text-interface-medium" Margin="4,0,0,0" /> <Border StrokeShape="RoundRectangle 12" StrokeThickness="0" StyleClass="bg-interface-softest" Padding="16"> <Border.Shadow> <Shadow Brush="Gray" Offset="5,5" Radius="5" Opacity="0.15" /> </Border.Shadow> <VerticalStackLayout Spacing="12"> <Picker x:Name="ThemeApplier" IsVisible="False"> <Picker.ItemsSource> <x:Array Type="{x:Type x:Object}"> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">Unspecified</AppTheme> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">Light</AppTheme> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">Dark</AppTheme> </x:Array> </Picker.ItemsSource> <Picker.SelectedItem> <Binding Source="{x:Static Application.Current}" Path="UserAppTheme" Mode="OneWayToSource" /> </Picker.SelectedItem> </Picker> <Border StrokeShape="RoundRectangle 14" StrokeThickness="0" StyleClass="bg-interface-softer" Padding="4"> <Grid ColumnDefinitions="*, *, *" ColumnSpacing="4"> {%- for option in themeOptions -%} {%- assign optionParts = option | Split:'|' -%} {%- assign optionIndex = forloop.index0 -%} <Border StrokeShape="RoundRectangle 10" StrokeThickness="0" BackgroundColor="Transparent" Padding="6,10" Grid.Column="{{ optionIndex }}"> <Border.Triggers> <DataTrigger TargetType="Border" Binding="{Binding Source={x:Static Application.Current}, Path=UserAppTheme}"> <DataTrigger.Value> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">{{ optionParts[0] }}</AppTheme> </DataTrigger.Value> <Setter Property="BackgroundColor" Value="{{ accentColor }}" /> <Setter Property="InputTransparent" Value="True" /> </DataTrigger> </Border.Triggers> <VerticalStackLayout Spacing="4" HorizontalOptions="Center"> <Rock:Icon IconClass="{{ optionParts[2] }}" FontSize="18" FontAutoScalingEnabled="False" StyleClass="text-interface-strong" HorizontalOptions="Center"> <Rock:Icon.Triggers> <DataTrigger TargetType="Rock:Icon" Binding="{Binding Source={x:Static Application.Current}, Path=UserAppTheme}"> <DataTrigger.Value> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">{{ optionParts[0] }}</AppTheme> </DataTrigger.Value> <Setter Property="TextColor" Value="White" /> </DataTrigger> </Rock:Icon.Triggers> </Rock:Icon> <Label Text="{{ optionParts[1] }}" StyleClass="footnote, bold, text-interface-strongest" HorizontalOptions="Center"> <Label.Triggers> <DataTrigger TargetType="Label" Binding="{Binding Source={x:Static Application.Current}, Path=UserAppTheme}"> <DataTrigger.Value> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">{{ optionParts[0] }}</AppTheme> </DataTrigger.Value> <Setter Property="TextColor" Value="White" /> </DataTrigger> </Label.Triggers> </Label> </VerticalStackLayout> <Border.GestureRecognizers> <TapGestureRecognizer Command="{Binding AggregateCommand}"> <TapGestureRecognizer.CommandParameter> <Rock:AggregateCommandParameters> <Rock:CommandReference Command="{Binding SetAppValue}" CommandParameter="{{ themeKey }}={{ optionIndex }}" /> <Rock:CommandReference Command="{Binding SetViewProperty}"> <Rock:SetViewPropertyParameters View="{x:Reference ThemeApplier}" Name="SelectedIndex" Value="{{ optionIndex }}" /> </Rock:CommandReference> </Rock:AggregateCommandParameters> </TapGestureRecognizer.CommandParameter> </TapGestureRecognizer> </Border.GestureRecognizers> </Border> {%- endfor -%} </Grid> </Border> </VerticalStackLayout> </Border> </VerticalStackLayout> Set these block settings and save: Setting Value Dynamic Content On Process Lava On Server On Process Lava On Client Off Enabled Lava Commands None Deploy the app. Rock only sends new blocks and block settings to phones on a Deploy. Step 3 - Try the Card Open the app, go to Theme Test, and tap whichever of Light or Dark your phone isn't using. The app changes theme and reopens on its start page. That's normal, it happens every time the theme changes. Go back to Theme Test. The option you tapped should be highlighted. Close the app completely and open it again. It's back on the phone's theme, which is what the next step fixes. Step 4 - Add the Theme Block Still on Theme Test, add a second Content block named Theme. Where it goes on the page doesn't matter, since it doesn't show anything. Paste this into its Content: {% raw %} {%- assign savedTheme = AppValues.ThemeIndex | Default:'0' | AsInteger -%} <ContentView IsVisible="False"> {%- if savedTheme == 1 or savedTheme == 2 -%} <Picker> <Picker.ItemsSource> <x:Array Type="{x:Type x:Object}"> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">Unspecified</AppTheme> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">Light</AppTheme> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">Dark</AppTheme> </x:Array> </Picker.ItemsSource> <Picker.SelectedItem> <Binding Source="{x:Static Application.Current}" Path="UserAppTheme" Mode="OneWayToSource" /> </Picker.SelectedItem> <Picker.SelectedIndex> <x:Int32>{{ savedTheme }}</x:Int32> </Picker.SelectedIndex> </Picker> {%- endif -%} </ContentView> {% endraw %} Set these block settings and save: Setting Value Dynamic Content On Process Lava On Server On Process Lava On Client On Enabled Lava Commands None CSS Class Blank Deploy the app. Double check Dynamic Content on this one. With it off, the page shows Xml_InvalidRootData, 1, 1. Adding collapse to the CSS Class hides the error but leaves an empty gap, so leave CSS Class blank and turn Dynamic Content on instead. Step 5 - Test a Restart On the card, pick Light or Dark, whichever is opposite your phone. Close the app completely and open it again. Go to Theme Test. The app changes to your saved choice and reopens on the start page, once. Open Theme Test again. This time nothing happens, which is what you want. Tap System to go back to following the phone. On the test page the saved choice is applied when you open Theme Test. Once the Theme block is on your start page in the next step, it's applied as the app opens. Step 6 - Move the Blocks to Their Real Pages Add the Appearance card to your settings page (an account or profile page works well) with the same block settings as Step 2. To add it to a Content block that's already on that page instead, see "Customize It" below. Add the Theme block to your app's start page, the page the app opens to, with the same block settings as Step 4. Ours sits after the start page's main Content block. Remove both blocks from Theme Test, and remove the link from Step 1. You can keep the page as a sandbox. Deploy, then repeat the restart test from Step 5. Keep the Theme block as its own block on the start page instead of pasting it into the start page's main Content block. It needs Lava on the client, and turning that on for a large block slows down every load of that page. Done. Your app has an Appearance setting, and it remembers the choice the next time the app opens. Customize It to Your Instance The three lines at the top of the card hold the settings you'd most likely change: {%- assign accentColor = '#2563EB' -%} {%- assign themeKey = 'ThemeIndex' -%} {%- assign themeOptions = 'Unspecified|System|adjust,Light|Light|sun,Dark|Dark|moon' | Split:',' -%} To change this Do this Highlight color #2563EB is a placeholder. Set accentColor to your brand color. The selected option's icon and label are white, so use a color dark enough for white text. Labels and icons Each option is Value|Label|Icon. Change the label ("Auto" instead of "System", for example) and the icon (any Font Awesome name). Don't change the value or the order, since the position is what gets saved. Saved key themeKey is the name the choice is saved under on the phone. If you change it, change AppValues.ThemeIndex in the Theme block to match. A prefix like your church's initials keeps it from colliding with other keys. Card colors and text The card uses Rock's classes: bg-interface-softest for the card, bg-interface-softer behind the options, text-interface-* for text, and footnote for text size. These change with dark mode on their own. If you swap in your own classes, give each one a .dark-mode rule. Adding it to an existing block Paste the whole card, assign lines included, into the Content block where you want it. That block needs Process Lava On Server on, and nothing else on the page can be named ThemeApplier. Good to Know: The App Reopens on the Start Page Every time the theme changes, the Rock Mobile shell rebuilds the app on its start page. There's no setting for it. Tapping Light or Dark on the card takes the person to the start page in the new theme. If the saved choice is different from the phone's setting, the app opens in the phone's theme and changes as the start page loads. Tapping System when the app already matches the phone doesn't change anything, so the app stays put. How It Works You don't need this section to set it up. It's here for anyone who wants to change the code or understand why it's built this way. The Theme Switch Rock Mobile runs on .NET MAUI, and MAUI controls the app's theme with Application.Current.UserAppTheme. Setting it to Light or Dark overrides the phone, and Unspecified goes back to following the phone. A page's XAML can set it, but two things get in the way. 1. AppTheme isn't in Rock's XAML namespace A namespace prefix inside {...} (like {x:Static ...}) fails with "No xmlns declaration for prefix", and Rock ignores an xmlns declared inside your content. An element tag does work, so each value is built as its own element with its own default namespace: <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">Dark</AppTheme> 2. A binding can't turn text or a number into an AppTheme Binding a Picker of strings, or SelectedIndex, directly to UserAppTheme does nothing, the conversion fails without an error. So the Picker holds the real AppTheme values from #1, and its SelectedItem is bound one way into UserAppTheme: <Picker x:Name="ThemeApplier" IsVisible="False"> <Picker.ItemsSource> <x:Array Type="{x:Type x:Object}"> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">Unspecified</AppTheme> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">Light</AppTheme> <AppTheme xmlns="clr-namespace:Microsoft.Maui.ApplicationModel;assembly=Microsoft.Maui.Essentials">Dark</AppTheme> </x:Array> </Picker.ItemsSource> <Picker.SelectedItem> <Binding Source="{x:Static Application.Current}" Path="UserAppTheme" Mode="OneWayToSource" /> </Picker.SelectedItem> </Picker> Selecting index 2 in that Picker sets the app to Dark. The rest of the recipe saves that choice and applies it again when the app opens. The Appearance Card, Part by Part The three assign lines are the settings from "Customize It". The APPEARANCE label and the outer Border are the card's header and box. The hidden Picker named ThemeApplier changes the theme, using both tricks above. The item order matters: Unspecified is 0, Light is 1 and Dark is 2, and those numbers are what gets saved. The for loop builds the three options from themeOptions, splitting each one on | into its value, label and icon. forloop.index0 gives each option its number, which sets its grid column and what it saves. Each option has a DataTrigger bound to UserAppTheme. When the app's theme matches the option's value, the trigger sets the background to accentColor and turns on InputTransparent, so the selected option is highlighted and ignores taps. The icon and label have the same trigger to turn their text white. A tap runs AggregateCommand, which runs two commands in order: SetAppValue saves ThemeIndex=N on the phone, then SetViewProperty sets ThemeApplier's SelectedIndex to N, which applies the theme. The Theme Block, Part by Part MAUI doesn't remember UserAppTheme after the app closes, but the saved value in AppValues does stay. This block reads it and applies it again. Everything is inside {% raw %}, so the server passes it through and the phone runs the Lava. AppValues only exist on the phone, which is why this block needs Process Lava On Client. It also needs Dynamic Content on. With it off, the content is bundled into the app, client Lava never runs on it, and the raw Lava reaches the XAML parser, which is where Xml_InvalidRootData, 1, 1 comes from. savedTheme reads AppValues.ThemeIndex, uses 0 (System) if nothing is saved, and converts it to a number. The ContentView is hidden, so the block doesn't show anything or take up space. The if only renders the Picker for a saved Light (1) or Dark (2). For System there's nothing to do, since the app follows the phone when it opens. The Picker is the same as the card's, plus Picker.SelectedIndex set to savedTheme. It comes after ItemsSource so the items exist before one is selected. It doesn't loop. After the app reopens on the start page the block runs again, but setting the theme the app already has doesn't change anything. Troubleshooting Start page shows Xml_InvalidRootData, 1, 1. The Theme block has Dynamic Content off. Turn it on and Deploy. An error mentioning Arg_ObjObjEx. That's Rock's error for client Lava in a block with Process Lava On Client off. Turn it on for the Theme block. Tapping an option does nothing. Check that the hidden Picker is still named ThemeApplier and is in the same block as the options. The choice is gone after a restart. The Theme block isn't on the page the app opens to, or AppValues.ThemeIndex doesn't match themeKey on the card. The app goes to the start page every time a page opens. Something is applying the theme without the saved-choice if. Use the Theme block as written, and only once in the app. New blocks or settings don't show up on the phone. Deploy, then close and reopen the app. That's It! Six steps and you've got an Appearance setting in your app. People can pick light or dark for themselves, and the app remembers it. Download File