Skip to content

Jitter when removing / adding images to markdown text #415

@jonmindtrip

Description

@jonmindtrip

I have markdown text with multiple inline images - example: An image ![sunset](sunset) within ![cloud](cloud) a line of text. And this works fine:

Image

However, I also have logic that adds/removes images from within the markdown. If I remove the cloud image, making the markdown: An image ![sunset](sunset) within a line of text., the cloud image is correctly removed. However, the sunset image is also briefly removed from the rendered text which causes some flickering and layout artifacts:

Simulator.Screen.Recording.-.iPhone.16e.-.2025-07-17.at.14.22.36.mp4

There are two problems in this video: see the "end" label wiggles up and down, because the Markdown height is slightly higher once all images are loaded. You can also see the word "within" shift to the left incorrectly; we never removed the sunset image from the markdown, but because InlineImageProvider is async, the first render of the label doesn't yet have any images, so the "within" is shifted to the left.

Checklist

  • [x ] I can reproduce this issue with a vanilla SwiftUI project.
  • [x ] I can reproduce this issue using the main branch of this package.
  • [x ] This bug hasn't been addressed in an existing GitHub issue.

Steps to reproduce

The following standalone code shows the issue:

import MarkdownUI
import SwiftUI

let one = "An image ![sunset](sunset) within a line of text."
let two = "An image ![sunset](sunset) within ![cloud](cloud) a line of text."

struct SystemImageInlineImageProvider: InlineImageProvider {
  private let name: (URL) -> String
  public init(name: @escaping (URL) -> String = \.lastPathComponent) {
    self.name = name
  }

  public func image(with url: URL, label: String) async throws -> Image {
    // Add an artificial delay to make the issue more obvious.
    try await Task.sleep(for: .seconds(0.2))
    return .init(systemName: self.name(url))
  }
}

extension InlineImageProvider where Self == SystemImageInlineImageProvider {
  static var system: Self { .init() }
}

struct ImageFlickerView: View {
  @State var currentContent: String = one
  var body: some View {
    ScrollView {
      VStack {
        Text("start")
          .frame(maxWidth: .infinity, alignment: .leading)
          .padding(.bottom, 24)

        Markdown(currentContent)
          .markdownInlineImageProvider(SystemImageInlineImageProvider())
          .frame(maxWidth: .infinity, alignment: .leading)

        Text("end")
          .frame(maxWidth: .infinity, alignment: .leading)
          .padding(.top, 24)
        Spacer()
      }
      .onTapGesture {
        currentContent = currentContent == one ? two : one
      }
      .padding(.horizontal, 24)
    }
  }
}

Version information

  • MarkdownUI: current
  • OS: iOS 18
  • Xcode: 16

Additional context
My suggestion would be to add a function func placeholder() -> Text? to InlineImageProvider, with a default impl that returns nil. TextInlineRenderer.renderImage(_:) would then have access to this placeholder, and would use that until the image is loaded.

I have this change working locally and would be happy to raise a PR for it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions