From 091f8e96deb706e5d33bad548b8db9c150f11f3d Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Fri, 14 Aug 2026 17:08:09 +0100 Subject: [PATCH] Sync iOS glass controls with app theme Signed-off-by: kenny lopez --- .../ios/Runner/JumpToLatestGlassButton.swift | 29 +++++++++ mobile/ios/Runner/StickyDateGlassHeader.swift | 22 +++++-- .../channels/jump_to_latest_button.dart | 12 ++++ .../features/channels/sticky_date_header.dart | 9 ++- .../channels/jump_to_latest_button_test.dart | 45 +++++++++++++- .../channels/sticky_date_header_test.dart | 60 ++++++++++++++++++- 6 files changed, 167 insertions(+), 10 deletions(-) diff --git a/mobile/ios/Runner/JumpToLatestGlassButton.swift b/mobile/ios/Runner/JumpToLatestGlassButton.swift index 790c154e7..0e96a78ec 100644 --- a/mobile/ios/Runner/JumpToLatestGlassButton.swift +++ b/mobile/ios/Runner/JumpToLatestGlassButton.swift @@ -9,6 +9,10 @@ final class JumpToLatestGlassButtonFactory: NSObject, FlutterPlatformViewFactory super.init() } + func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { + FlutterStandardMessageCodec.sharedInstance() + } + func create( withFrame frame: CGRect, viewIdentifier viewId: Int64, @@ -17,6 +21,7 @@ final class JumpToLatestGlassButtonFactory: NSObject, FlutterPlatformViewFactory JumpToLatestGlassButtonPlatformView( frame: frame, viewIdentifier: viewId, + arguments: args, messenger: messenger ) } @@ -43,6 +48,7 @@ final class JumpToLatestGlassButtonPlatformView: NSObject, FlutterPlatformView { init( frame: CGRect, viewIdentifier viewId: Int64, + arguments args: Any?, messenger: FlutterBinaryMessenger ) { containerView = UIView(frame: frame) @@ -54,6 +60,7 @@ final class JumpToLatestGlassButtonPlatformView: NSObject, FlutterPlatformView { containerView.backgroundColor = .clear containerView.isOpaque = false + applyBrightness(from: args) var configuration: UIButton.Configuration if #available(iOS 26.0, *) { @@ -81,6 +88,15 @@ final class JumpToLatestGlassButtonPlatformView: NSObject, FlutterPlatformView { for: .touchUpInside ) + channel.setMethodCallHandler { [weak self] call, result in + guard call.method == "setBrightness" else { + result(FlutterMethodNotImplemented) + return + } + self?.applyBrightness(from: call.arguments) + result(nil) + } + containerView.addSubview(button) NSLayoutConstraint.activate([ button.centerXAnchor.constraint(equalTo: containerView.centerXAnchor), @@ -93,4 +109,17 @@ final class JumpToLatestGlassButtonPlatformView: NSObject, FlutterPlatformView { func view() -> UIView { containerView } + + private func applyBrightness(from value: Any?) { + let brightness = (value as? [String: Any])?["brightness"] as? String + ?? value as? String + let interfaceStyle: UIUserInterfaceStyle = brightness == "dark" ? .dark : .light + containerView.overrideUserInterfaceStyle = interfaceStyle + button.overrideUserInterfaceStyle = interfaceStyle + button.setNeedsUpdateConfiguration() + } + + deinit { + channel.setMethodCallHandler(nil) + } } diff --git a/mobile/ios/Runner/StickyDateGlassHeader.swift b/mobile/ios/Runner/StickyDateGlassHeader.swift index f95ec0555..6a502048a 100644 --- a/mobile/ios/Runner/StickyDateGlassHeader.swift +++ b/mobile/ios/Runner/StickyDateGlassHeader.swift @@ -69,6 +69,7 @@ final class StickyDateGlassHeaderPlatformView: NSObject, FlutterPlatformView { glassView.isUserInteractionEnabled = false glassView.clipsToBounds = true glassView.layer.cornerCurve = .continuous + applyBrightness(from: arguments?["brightness"]) dateLabel.translatesAutoresizingMaskIntoConstraints = false dateLabel.text = text @@ -98,12 +99,20 @@ final class StickyDateGlassHeaderPlatformView: NSObject, FlutterPlatformView { ]) channel.setMethodCallHandler { [weak self] call, result in - guard call.method == "setLabel", let text = call.arguments as? String else { + switch call.method { + case "setLabel": + guard let text = call.arguments as? String else { + result(FlutterMethodNotImplemented) + return + } + self?.dateLabel.text = text + result(nil) + case "setBrightness": + self?.applyBrightness(from: call.arguments) + result(nil) + default: result(FlutterMethodNotImplemented) - return } - self?.dateLabel.text = text - result(nil) } } @@ -111,6 +120,11 @@ final class StickyDateGlassHeaderPlatformView: NSObject, FlutterPlatformView { glassView } + private func applyBrightness(from value: Any?) { + let interfaceStyle: UIUserInterfaceStyle = value as? String == "dark" ? .dark : .light + glassView.overrideUserInterfaceStyle = interfaceStyle + } + deinit { channel.setMethodCallHandler(nil) } diff --git a/mobile/lib/features/channels/jump_to_latest_button.dart b/mobile/lib/features/channels/jump_to_latest_button.dart index 47dd6a1b8..314fb624e 100644 --- a/mobile/lib/features/channels/jump_to_latest_button.dart +++ b/mobile/lib/features/channels/jump_to_latest_button.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:ui'; import 'package:flutter/foundation.dart'; @@ -21,6 +22,7 @@ class JumpToLatestButton extends HookWidget { Widget build(BuildContext context) { final nativeChannel = useState(null); final onPressedRef = useRef(onPressed)..value = onPressed; + final brightness = context.theme.brightness.name; useEffect(() { final channel = nativeChannel.value; @@ -31,6 +33,14 @@ class JumpToLatestButton extends HookWidget { return () => channel.setMethodCallHandler(null); }, [nativeChannel.value]); + useEffect(() { + final channel = nativeChannel.value; + if (channel != null) { + unawaited(channel.invokeMethod('setBrightness', brightness)); + } + return null; + }, [nativeChannel.value, brightness]); + final borderColor = context.colors.onSurface.withValues(alpha: 0.08); final usesNativeIosGlass = defaultTargetPlatform == TargetPlatform.iOS; @@ -47,6 +57,8 @@ class JumpToLatestButton extends HookWidget { key: const ValueKey('channel-jump-to-latest-ios-glass'), viewType: _iosViewType, hitTestBehavior: PlatformViewHitTestBehavior.opaque, + creationParams: {'brightness': brightness}, + creationParamsCodec: const StandardMessageCodec(), onPlatformViewCreated: (viewId) { nativeChannel.value = MethodChannel( '$_iosViewType/$viewId', diff --git a/mobile/lib/features/channels/sticky_date_header.dart b/mobile/lib/features/channels/sticky_date_header.dart index 3b7250045..cad7f452b 100644 --- a/mobile/lib/features/channels/sticky_date_header.dart +++ b/mobile/lib/features/channels/sticky_date_header.dart @@ -168,14 +168,16 @@ class _IosStickyDateGlass extends HookWidget { @override Widget build(BuildContext context) { final nativeChannel = useState(null); + final brightness = context.theme.brightness.name; useEffect(() { final channel = nativeChannel.value; if (channel != null) { unawaited(channel.invokeMethod('setLabel', label)); + unawaited(channel.invokeMethod('setBrightness', brightness)); } return null; - }, [nativeChannel.value, label]); + }, [nativeChannel.value, label, brightness]); return Semantics( header: true, @@ -189,7 +191,10 @@ class _IosStickyDateGlass extends HookWidget { key: const ValueKey('channel-sticky-date-header-ios-glass'), viewType: StickyDateHeader._iosViewType, hitTestBehavior: PlatformViewHitTestBehavior.transparent, - creationParams: {'label': label}, + creationParams: { + 'label': label, + 'brightness': brightness, + }, creationParamsCodec: const StandardMessageCodec(), onPlatformViewCreated: (viewId) { nativeChannel.value = MethodChannel( diff --git a/mobile/test/features/channels/jump_to_latest_button_test.dart b/mobile/test/features/channels/jump_to_latest_button_test.dart index a8880a0df..b934f83aa 100644 --- a/mobile/test/features/channels/jump_to_latest_button_test.dart +++ b/mobile/test/features/channels/jump_to_latest_button_test.dart @@ -2,11 +2,22 @@ import 'package:buzz/features/channels/jump_to_latest_button.dart'; import 'package:buzz/shared/theme/theme.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - testWidgets('uses the native glass control on iOS', (tester) async { + testWidgets('keeps the native iOS glass in sync with the app theme', ( + tester, + ) async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + const channel = MethodChannel('buzz/jump_to_latest_glass/41'); + final methodCalls = []; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, ( + call, + ) async { + methodCalls.add(call); + return null; + }); try { await tester.pumpWidget( MaterialApp( @@ -17,11 +28,43 @@ void main() { final nativeView = tester.widget(find.byType(UiKitView)); expect(nativeView.viewType, 'buzz/jump_to_latest_glass'); + expect(nativeView.creationParams, { + 'brightness': 'light', + }); expect( find.byKey(const ValueKey('channel-jump-to-latest-ios-glass')), findsOneWidget, ); + + nativeView.onPlatformViewCreated!(41); + await tester.pump(); + expect( + methodCalls + .lastWhere((call) => call.method == 'setBrightness') + .arguments, + 'light', + ); + + methodCalls.clear(); + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.dark(), + home: Scaffold(body: JumpToLatestButton(onPressed: () {})), + ), + ); + await tester.pumpAndSettle(); + + expect( + methodCalls + .lastWhere((call) => call.method == 'setBrightness') + .arguments, + 'dark', + ); } finally { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + null, + ); debugDefaultTargetPlatformOverride = null; } }); diff --git a/mobile/test/features/channels/sticky_date_header_test.dart b/mobile/test/features/channels/sticky_date_header_test.dart index 53b08dd33..c6c012ba0 100644 --- a/mobile/test/features/channels/sticky_date_header_test.dart +++ b/mobile/test/features/channels/sticky_date_header_test.dart @@ -2,14 +2,25 @@ import 'package:buzz/features/channels/sticky_date_header.dart'; import 'package:buzz/shared/theme/theme.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - testWidgets('uses native glass and updates its date on iOS', (tester) async { + testWidgets('updates the native iOS glass date and app theme', ( + tester, + ) async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; final state = ValueNotifier( const StickyDateHeaderState(label: 'Yesterday'), ); + const channel = MethodChannel('buzz/sticky_date_glass/42'); + final methodCalls = []; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, ( + call, + ) async { + methodCalls.add(call); + return null; + }); try { await tester.pumpWidget( MaterialApp( @@ -20,15 +31,58 @@ void main() { var nativeView = tester.widget(find.byType(UiKitView)); expect(nativeView.viewType, 'buzz/sticky_date_glass'); - expect(nativeView.creationParams, {'label': 'Yesterday'}); + expect(nativeView.creationParams, { + 'label': 'Yesterday', + 'brightness': 'light', + }); expect(find.byType(BackdropFilter), findsNothing); + nativeView.onPlatformViewCreated!(42); + await tester.pump(); + expect( + methodCalls.lastWhere((call) => call.method == 'setLabel').arguments, + 'Yesterday', + ); + expect( + methodCalls + .lastWhere((call) => call.method == 'setBrightness') + .arguments, + 'light', + ); + state.value = const StickyDateHeaderState(label: 'Today'); await tester.pump(); nativeView = tester.widget(find.byType(UiKitView)); - expect(nativeView.creationParams, {'label': 'Today'}); + expect(nativeView.creationParams, { + 'label': 'Today', + 'brightness': 'light', + }); + expect( + methodCalls.lastWhere((call) => call.method == 'setLabel').arguments, + 'Today', + ); + + methodCalls.clear(); + await tester.pumpWidget( + MaterialApp( + theme: AppTheme.dark(), + home: Scaffold(body: StickyDateHeader(state: state)), + ), + ); + await tester.pumpAndSettle(); + + expect( + methodCalls + .lastWhere((call) => call.method == 'setBrightness') + .arguments, + 'dark', + ); } finally { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + null, + ); state.dispose(); debugDefaultTargetPlatformOverride = null; }