Sync iOS glass controls with app theme

Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
kenny lopez
2026-08-14 19:00:36 +01:00
parent d7e6578ba9
commit 091f8e96de
6 changed files with 167 additions and 10 deletions
@@ -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)
}
}
+18 -4
View File
@@ -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)
}
@@ -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<MethodChannel?>(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<void>('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: <String, Object>{'brightness': brightness},
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated: (viewId) {
nativeChannel.value = MethodChannel(
'$_iosViewType/$viewId',
@@ -168,14 +168,16 @@ class _IosStickyDateGlass extends HookWidget {
@override
Widget build(BuildContext context) {
final nativeChannel = useState<MethodChannel?>(null);
final brightness = context.theme.brightness.name;
useEffect(() {
final channel = nativeChannel.value;
if (channel != null) {
unawaited(channel.invokeMethod<void>('setLabel', label));
unawaited(channel.invokeMethod<void>('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: <String, Object>{'label': label},
creationParams: <String, Object>{
'label': label,
'brightness': brightness,
},
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated: (viewId) {
nativeChannel.value = MethodChannel(
@@ -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 = <MethodCall>[];
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<UiKitView>(find.byType(UiKitView));
expect(nativeView.viewType, 'buzz/jump_to_latest_glass');
expect(nativeView.creationParams, <String, Object>{
'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;
}
});
@@ -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 = <MethodCall>[];
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<UiKitView>(find.byType(UiKitView));
expect(nativeView.viewType, 'buzz/sticky_date_glass');
expect(nativeView.creationParams, <String, Object>{'label': 'Yesterday'});
expect(nativeView.creationParams, <String, Object>{
'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<UiKitView>(find.byType(UiKitView));
expect(nativeView.creationParams, <String, Object>{'label': 'Today'});
expect(nativeView.creationParams, <String, Object>{
'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;
}