Repository: flutter
Issue: Text with ghosting effect on my web application just on iPhones with iOS 18.0 or higher
Labels: waiting for customer response,in triage

### Steps to reproduce

Just run the application and do the scroll on the screen. The title will be with the ghosting effect

### Expected results

The scroll shouldn't cause the ghosting effect

### Actual results

The scroll causes the ghosting effect just on iOS 18.0 or higher. I've tested on previous versions and the scroll behavior it normal.

### Code sample


```dart
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

// --------------- MAIN ---------------

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Mock MySessionsPage',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MySessionsPage(),
    );
  }
}
```

// --------------- MOCK MODELS ---------------


```dart
class Bot {
  Bot({required this.name, this.avatarUrl});
  final String name;
  final String? avatarUrl;
}

class Scenario {
  Scenario({
    required this.id,
    required this.name,
    this.description,
    required this.duration,
    this.bot,
    this.totalSessions = 0,
    this.maxSessions,
  });
  final String id;
  final String name;
  final String? description;
  final double duration; // in seconds
  final Bot? bot;
  final int totalSessions;
  final int? maxSessions;
}

class Session {
  Session({
    required this.id,
    this.scenario,
    required this.time,
    this.isCompleted = false,
    this.hasReports = false,
    this.startedAt,
    this.completedAt,
  });
  final String id;
  final Scenario? scenario;
  final int time; // in seconds
  final bool isCompleted;
  final bool hasReports;
  final DateTime? startedAt;
  final DateTime? completedAt;
}
```

// --------------- MOCK DATA ---------------

```dart
final mockScenario1 = Scenario(
  id: 'scenario-1',
  name: 'Scenario Alpha',
  description: 'This scenario tests your response to Alpha Bot’s conversation.',
  duration: 10 * 60, // 10 minutes in seconds
  bot: Bot(
    name: 'Alpha Bot',
    avatarUrl: 'https://via.placeholder.com/200x200?text=Alpha+Bot',
  ),
  totalSessions: 1,
  maxSessions: 5,
);

final mockScenario2 = Scenario(
  id: 'scenario-2',
  name: 'Scenario Beta',
  description: 'A scenario about Beta Bot. Great for practicing dialogues.',
  duration: 5 * 60, // 5 minutes in seconds
  bot: Bot(
    name: 'Beta Bot',
    avatarUrl: 'https://via.placeholder.com/200x200?text=Beta+Bot',
  ),
  totalSessions: 2,
  maxSessions: 3,
);

final mockInProgressSessions = [
  Session(
    id: 'session-in-progress-1',
    scenario: mockScenario1,
    time: 300, // 5 min
    startedAt: DateTime.now().subtract(const Duration(minutes: 5)),
  ),
];

final mockNextSessions = [
  Session(
    id: 'session-next-1',
    scenario: mockScenario2,
    time: 60, // 1 min
    // Not started yet
  ),
];

final mockPastSessions = [
  Session(
    id: 'session-past-1',
    scenario: mockScenario1,
    time: 250,
    isCompleted: true,
    hasReports: true,
    startedAt: DateTime.now().subtract(const Duration(days: 2, minutes: 10)),
    completedAt: DateTime.now().subtract(const Duration(days: 2)),
  ),
  Session(
    id: 'session-past-2',
    scenario: mockScenario2,
    time: 80,
    isCompleted: true,
    startedAt: DateTime.now().subtract(const Duration(days: 1, minutes: 5)),
    completedAt: DateTime.now().subtract(const Duration(days: 1)),
  ),
];
```

// --------------- MOCK UTILS ---------------

```dart
Future<void> openSessionReportsModal(BuildContext context, String sessionId,
    {bool openedByLink = false}) async {
  // Just a mock dialog to represent "open session reports" action
  return showDialog(
    context: context,
    builder: (ctx) => AlertDialog(
      title: const Text('Session Report'),
      content: Text('Opened report for sessionId=$sessionId\nopenedByLink=$openedByLink'),
      actions: [
        TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('OK')),
      ],
    ),
  );
}

String formatSecondsToMinutes(int seconds) {
  final minutes = (seconds / 60).ceil();
  return '$minutes min';
}

extension DateTimeFormatter on DateTime {
  String formatDateTime() {
    // Very simple formatter
    return '$year-$month-$day ${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
  }
}
```

// --------------- MOCK PAGE ---------------


```dart
class MySessionsPage extends StatelessWidget {
  const MySessionsPage({super.key});

  @override
  Widget build(BuildContext context) {
    final inProgressScrollCtrl = ScrollController();
    final nextScrollCtrl = ScrollController();

    final hasInProgress = mockInProgressSessions.isNotEmpty;
    final hasNext = mockNextSessions.isNotEmpty;
    final hasPast = mockPastSessions.isNotEmpty;

    // Desktop vs Mobile checking for example - you can tweak this condition
    final isDesktop = MediaQuery.of(context).size.width > 600;

    return Scaffold(
      appBar: AppBar(title: const Text('My Sessions')),
      body: SingleChildScrollView(
        child: Column(
          children: [
            // If no sessions found at all
            if (!hasInProgress && !hasNext && !hasPast)
              const EmptyState(
                title: 'No Training Available',
                body: 'You have no sessions available.',
              )
            else ...[
              // In progress sessions
              if (hasInProgress) ...[
                const _Header(text: 'In Progress', hPadding: 16),
                SizedBox(
                  height: 300,
                  child: Scrollbar(
                    controller: inProgressScrollCtrl,
                    thumbVisibility: true,
                    child: ListView.builder(
                      controller: inProgressScrollCtrl,
                      padding: const EdgeInsets.symmetric(horizontal: 8),
                      scrollDirection: Axis.horizontal,
                      itemCount: mockInProgressSessions.length,
                      itemBuilder: (context, index) {
                        final session = mockInProgressSessions[index];
                        return _SessionCard(session: session);
                      },
                    ),
                  ),
                ),
              ],
              // Next sessions
              if (hasNext) ...[
                if (isDesktop) const SizedBox(height: 16),
                const _Header(text: 'Next Sessions', hPadding: 16),
                SizedBox(
                  height: 300,
                  child: Scrollbar(
                    controller: nextScrollCtrl,
                    thumbVisibility: true,
                    child: ListView.builder(
                      controller: nextScrollCtrl,
                      padding: const EdgeInsets.symmetric(horizontal: 8),
                      scrollDirection: Axis.horizontal,
                      itemCount: mockNextSessions.length,
                      itemBuilder: (context, index) {
                        final session = mockNextSessions[index];
                        return _SessionCard(session: session);
                      },
                    ),
                  ),
                ),
              ],
              // Past sessions
              if (hasPast) ...[
                const _Header(text: 'Past Sessions', hPadding: 16),
                if (isDesktop)
                  _PastSessions(sessions: mockPastSessions)
                else
                  _SmallPastSessions(sessions: mockPastSessions),
              ],
            ],
          ],
        ),
      ),
    );
  }
}
```

// --------------- SUB-WIDGETS ---------------

```dart
class _Header extends StatelessWidget {
  const _Header({
    required this.text,
    required this.hPadding,
  });

  final String text;
  final double hPadding;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.fromLTRB(hPadding, 8, hPadding, 16),
      child: Text(
        text,
        style: Theme.of(context).textTheme.headlineMedium,
      ),
    );
  }
}

class _SessionCard extends StatelessWidget {
  const _SessionCard({
    required this.session,
  });

  final Session session;

  @override
  Widget build(BuildContext context) {
    final scenario = session.scenario;
    final theme = Theme.of(context);

    // For demonstration, just convert the scenario's description to plain text
    final description = (scenario?.description ?? '').replaceAll('\n', ' ').trim();

    return SizedBox(
      width: 248,
      child: Card(
        margin: const EdgeInsets.all(8),
        child: InkWell(
          onTap: () {
            // Mock: go to ChatPage
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Tapped session: ${session.id}')),
            );
          },
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // "Header" area with image
              Container(
                height: 140,
                color: Colors.grey[200],
                child: Stack(
                  fit: StackFit.expand,
                  children: [
                    if (scenario?.bot?.avatarUrl != null && scenario!.bot!.avatarUrl!.isNotEmpty)
                      CachedNetworkImage(
                        imageUrl: scenario.bot!.avatarUrl!,
                        imageBuilder: (context, imageProvider) => DecoratedBox(
                          decoration: BoxDecoration(
                            image: DecorationImage(
                              image: imageProvider,
                              fit: BoxFit.cover,
                              colorFilter: ColorFilter.mode(
                                Colors.black.withOpacity(0.3),
                                BlendMode.darken,
                              ),
                            ),
                          ),
                        ),
                      )
                    else
                      const Align(child: CircleAvatar(radius: 36)),
                    Align(
                      alignment: Alignment.topRight,
                      child: Padding(
                        padding: const EdgeInsets.all(12),
                        child: Container(
                          padding: const EdgeInsets.all(8),
                          decoration: const ShapeDecoration(
                            color: Colors.blue,
                            shape: StadiumBorder(),
                          ),
                          child: Text(
                            '${((scenario?.duration ?? 0) / 60).ceil()} min',
                            style: theme.textTheme.bodySmall?.copyWith(color: Colors.white),
                          ),
                        ),
                      ),
                    ),
                    Align(
                      alignment: Alignment.bottomLeft,
                      child: Padding(
                        padding: const EdgeInsets.fromLTRB(16, 0, 16, 6),
                        child: Text(
                          scenario?.bot?.name ?? 'Unknown Bot',
                          style: theme.textTheme.headlineMedium?.copyWith(color: Colors.white),
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                    ),
                  ],
                ),
              ),
              // Name
              Padding(
                padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
                child: Text(
                  scenario?.name ?? 'Unknown Scenario',
                  style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
                  overflow: TextOverflow.ellipsis,
                  maxLines: 2,
                ),
              ),
              // Description
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
                  child: Text(
                    description,
                    style: theme.textTheme.bodySmall?.copyWith(color: Colors.grey[700]),
                    overflow: TextOverflow.ellipsis,
                    maxLines: 3,
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
```

// Past sessions layout (big screen / table style)

```dart
class _PastSessions extends StatelessWidget {
  const _PastSessions({required this.sessions});

  final List<Session> sessions;

  @override
  Widget build(BuildContext context) {
    if (sessions.isEmpty) {
      return const Padding(
        padding: EdgeInsets.all(24),
        child: EmptyState(
          title: 'No Past Sessions',
          body: 'Try starting a new session first.',
        ),
      );
    }

    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: DataTable(
        columns: const [
          DataColumn(label: Text('Session')),
          DataColumn(label: Text('Time')),
          DataColumn(label: Text('Started At')),
          DataColumn(label: Text('Completed At')),
          DataColumn(label: Text('Actions')),
        ],
        rows: sessions.map((session) {
          return DataRow(
            cells: [
              DataCell(Text(session.scenario?.name ?? 'Unknown')),
              DataCell(Text(formatSecondsToMinutes(session.time))),
              DataCell(Text(session.startedAt?.formatDateTime() ?? '-')),
              DataCell(Text(session.completedAt?.formatDateTime() ?? '-')),
              DataCell(_Actions(session: session)),
            ],
          );
        }).toList(),
      ),
    );
  }
}
```

// Past sessions layout (small screen / card list style)

```dart
class _SmallPastSessions extends StatelessWidget {
  const _SmallPastSessions({required this.sessions});

  final List<Session> sessions;

  @override
  Widget build(BuildContext context) {
    if (sessions.isEmpty) {
      return const Padding(
        padding: EdgeInsets.all(24),
        child: EmptyState(
          title: 'No Past Sessions',
          body: 'Try starting a new session first.',
        ),
      );
    }

    return Column(
      children: sessions.map((session) {
        return Card(
          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: InkWell(
            onTap: session.isCompleted && session.hasReports
                ? () async => openSessionReportsModal(context, session.id)
                : null,
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(session.scenario?.name ?? 'Unknown Scenario',
                      style: const TextStyle(fontWeight: FontWeight.w600)),
                  const SizedBox(height: 6),
                  Text('Time: ${formatSecondsToMinutes(session.time)}'),
                  Text('Started: ${session.startedAt?.formatDateTime() ?? '-'}'),
                  Text('Completed: ${session.completedAt?.formatDateTime() ?? '-'}'),
                  const SizedBox(height: 8),
                  _Actions(session: session),
                ],
              ),
            ),
          ),
        );
      }).toList(),
    );
  }
}
```

// --------------- ACTIONS BUTTONS ---------------

```dart
class _Actions extends StatelessWidget {
  const _Actions({required this.session});
  final Session session;

  @override
  Widget build(BuildContext context) {
    final isRepeatAllowed = session.scenario?.maxSessions == null ||
        (session.scenario!.totalSessions < session.scenario!.maxSessions!);

    return Row(
      mainAxisAlignment: MainAxisAlignment.end,
      children: [
        // Open report if completed
        if (session.isCompleted && session.hasReports) ...[
          SizedBox(
            height: 36,
            child: OutlinedButton.icon(
              onPressed: () async => openSessionReportsModal(context, session.id),
              icon: const Icon(Icons.file_open_outlined, size: 20),
              label: const Text('Open Report'),
            ),
          ),
          const SizedBox(width: 8),
        ],
        // Repeat session
        if (isRepeatAllowed) ...[
          SizedBox(
            height: 36,
            child: OutlinedButton.icon(
              onPressed: () => _onRepeat(context),
              icon: const Icon(Icons.repeat, size: 20),
              label: const Text('Repeat'),
            ),
          ),
        ],
      ],
    );
  }

  void _onRepeat(BuildContext context) {
    // Mock repeat action
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Repeating scenario: ${session.scenario?.id}')),
    );
  }
}
```

// --------------- EMPTY STATE MOCK ---------------

```dart
class EmptyState extends StatelessWidget {
  const EmptyState({
    super.key,
    required this.title,
    required this.body,
  });
  final String title;
  final String body;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(48),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Icon(Icons.hourglass_empty, size: 60, color: Colors.grey),
            const SizedBox(height: 16),
            Text(
              title,
            ),
            const SizedBox(height: 8),
            Text(
              body,
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }
}
```

### Screenshots or Video

<details open>
<summary>Screenshots / Video demonstration</summary>

![Screenshot 2025-01-07 at 12 03 44](https://github.com/user-attachments/assets/d39215e7-06a2-4446-83ea-4f882cfb0a36)

https://github.com/user-attachments/assets/23c57ab1-909e-4872-9c6b-ac6bb17ae93c

</details>


### Logs

<details open><summary>Logs</summary>

```console
[Paste your logs here]
```

</details>


### Flutter Doctor output

<details open><summary>Doctor output</summary>

```console
flutter doctor -v
[!] Flutter (Channel [user-branch], 3.22.3, on macOS 14.6 23G80 darwin-x64, locale en-BR)
    ! Flutter version 3.22.3 on channel [user-branch] at /Users/eduardomuchak/development/flutter
      Currently on an unknown channel. Run `flutter channel` to switch to an official channel.
      If that doesn't fix the issue, reinstall Flutter by following instructions at https://flutter.dev/docs/get-started/install.
    ! Upstream repository unknown source is not a standard remote.
      Set environment variable "FLUTTER_GIT_URL" to unknown source to dismiss this error.
    • Framework revision b0850beeb2 (6 months ago), 2024-07-16 21:43:41 -0700
    • Engine revision 235db911ba
    • Dart version 3.4.4
    • DevTools version 2.34.3
    • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades.

[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at /Users/eduardomuchak/Library/Android/sdk
    • Platform android-34, build-tools 34.0.0
    • ANDROID_HOME = /Users/eduardomuchak/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.11+0-17.0.11b1207.24-11852314)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 16.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 16C5032a
    • CocoaPods version 1.14.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2024.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.11+0-17.0.11b1207.24-11852314)

[✓] IntelliJ IDEA Ultimate Edition (version 2024.1.1)
    • IntelliJ at /Applications/IntelliJ IDEA.app
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] IntelliJ IDEA Community Edition (version 2023.3.4)
    • IntelliJ at /Applications/IntelliJ IDEA CE.app
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] VS Code (version 1.96.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.102.0

[✓] Connected device (5 available)
    • iPhone 15 (mobile)     • F1B8A3E2-0850-4A1B-94BE-B5BD260ABE91 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-17-5 (simulator)
    • iPhone 16 Pro (mobile) • 35785FF3-DE18-4422-9FBE-F1692782C70B • ios            • com.apple.CoreSimulator.SimRuntime.iOS-18-0 (simulator)
    • iPhone 14 Pro (mobile) • 4467AED9-5438-434F-91DF-FC09E3180CAF • ios            • com.apple.CoreSimulator.SimRuntime.iOS-16-4 (simulator)
    • macOS (desktop)        • macos                                • darwin-x64     • macOS 14.6 23G80 darwin-x64
    • Chrome (web)           • chrome                               • web-javascript • Google Chrome 131.0.6778.205

[✓] Network resources
    • All expected network resources are available.

! Doctor found issues in 1 category.
```
</details>