Inexplicable white background below SwiftUI Webview

1 day ago 3
ARTICLE AD BOX

I'm working on a pair of Swift projects, and one of them, running on an iPad, refuses to cooperate with this webview. The image below shows the whitespace below the editor. This only appears when the user scrolls down to the bottom of the editor's content. For context, the editor locks the body height to 100vh when in landscape mode so this can display an editor in a side-by-side scrollable container, and this whitespace doesn't appear when the iPad is in portrait mode with the editor hidden.

Whitespace below editor

Also, I've enabled vim mode in the editor, and this seems to occur more frequently, almost exclusively when the editor is in vim mode and the user jumps to the bottom of the note's content.

This is my webview:

struct MarkdownTabView: View { @Binding var editingNote: NoteModel? @Binding var fullScreenCover: MainFullScreenCover? var onNavigateToNote: (NoteModel) -> Void = { _ in } @AppStorage(AppStorageKeys.editorThemeDark.rawValue) private var editorThemeDark: CodeSyntaxTheme = .dracula @AppStorage(AppStorageKeys.editorThemeLight.rawValue) private var editorThemeLight: CodeSyntaxTheme = .githubLight @AppStorage(AppStorageKeys.theme.rawValue) private var theme: WebViewTheme = .fluster @AppStorage(AppStorageKeys.editorKeymap.rawValue) private var editorKeymap: EditorKeymap = .base @AppStorage(AppStorageKeys.hasLaunchedPreviously.rawValue) private var hasPreviouslyLaunched: Bool = false @Environment(ThemeManager.self) private var themeManager: ThemeManager var editorContainer: MdxEditorWebviewContainer init( editingNote: Binding<NoteModel?>, editorContainer: MdxEditorWebviewContainer, onNavigateToNote: ((NoteModel) -> Void)?, fullScreenCover: Binding<MainFullScreenCover?> ) { self._editingNote = editingNote self._fullScreenCover = fullScreenCover self.editorContainer = editorContainer if onNavigateToNote != nil { self.onNavigateToNote = onNavigateToNote! } } var body: some View { if let editingNoteBinding = Binding($editingNote) { NavigationStack { MdxEditorWebview( url: Bundle.main.url( forResource: "index", withExtension: "html", subdirectory: "splitview_mdx_editor" )!, theme: $theme, editorThemeDark: $editorThemeDark, editorThemeLight: $editorThemeLight, editingNote: editingNoteBinding, editorKeymap: $editorKeymap, container: editorContainer, onNavigateToNote: onNavigateToNote, fullScreenCover: $fullScreenCover ) .frame(maxWidth: .infinity, maxHeight: .infinity) // .frame(width: geo.size.width, height: geo.size.height, alignment: .topLeading) // TODO: Remove this. This is just for easy development. .onAppear { if let parsedMdx = editingNote?.markdown .preParsedBody { editorContainer.setParsedEditorContentString( content: parsedMdx ) } UIScrollView.appearance().bounces = false UIScrollView.appearance().isScrollEnabled = false } .onDisappear { UIScrollView.appearance().bounces = true UIScrollView.appearance().isScrollEnabled = true } } } else { if hasPreviouslyLaunched { SelectNoteToContinueView() } else { ProgressView() .progressViewStyle(.circular) .scaleEffect(1.5) .tint(themeManager.theme.primary) } } } }

And the MdxEditorWebview:

@MainActor public struct MdxEditorWebviewInternal: UIViewRepresentable { @State private var webView: WKWebView = WKWebView( frame: .zero, configuration: getConfig() ) @State private var didSetInitialContent = false @State var haveSetInitialContent: Bool = false @Environment(\.openURL) var openURL @Environment(\.modelContext) var modelContext @Environment(\.colorScheme) var colorScheme @Environment(\.createDataHandler) var dataHandler @AppStorage(AppStorageKeys.webviewFontSize.rawValue) private var webviewFontSize: WebviewFontSize = .base let url: URL @Binding var show: Bool @Binding var theme: WebViewTheme @Binding var editorThemeDark: CodeSyntaxTheme @Binding var editorThemeLight: CodeSyntaxTheme @Binding var editingNote: NoteModel @Binding var editorKeymap: EditorKeymap @Binding var fullScreenCover: MainFullScreenCover? var onNavigateToNote: (NoteModel) -> Void let container: MdxEditorWebviewContainer public init( url: URL, theme: Binding<WebViewTheme>, editorThemeDark: Binding<CodeSyntaxTheme>, editorThemeLight: Binding<CodeSyntaxTheme>, editingNote: Binding<NoteModel>, editorKeymap: Binding<EditorKeymap>, container: MdxEditorWebviewContainer, show: Binding<Bool>, onNavigateToNote: @escaping (NoteModel) -> Void, fullScreenCover: Binding<MainFullScreenCover?> ) { self.url = url self._theme = theme self._editorThemeDark = editorThemeDark self._editorThemeLight = editorThemeLight self._editingNote = editingNote self._editorKeymap = editorKeymap self._fullScreenCover = fullScreenCover self.container = container self._show = show self.onNavigateToNote = onNavigateToNote } public func makeUIView(context: Context) -> WKWebView { let webView = container.webView webView.isHidden = true webView.navigationDelegate = context.coordinator let editorContentControllers = [ SplitviewEditorWebviewActions.setWebviewLoaded.rawValue, SplitviewEditorWebviewActions.onEditorChange.rawValue, SplitviewEditorWebviewActions.requestSplitviewEditorData.rawValue, SplitviewEditorWebviewActions.requestParsedMdxContent.rawValue, SplitviewEditorWebviewActions.onTagClick.rawValue, MdxPreviewWebviewActions.viewNoteByUserDefinedId.rawValue, MdxPreviewWebviewActions.requestNoteData.rawValue ] if colorScheme == .dark { webView.evaluateJavaScript( """ document.body.classList.add("dark"); null; """ ) } for controllerName in editorContentControllers { addUserContentController( controller: webView.configuration.userContentController, coordinator: context.coordinator, name: controllerName ) } // Loading the page only once webView.loadFileURL(url, allowingReadAccessTo: url) if colorScheme == .dark { webView.evaluateJavaScript( """ document.body.classList.add("dark"); null; """ ) } return webView } public func updateUIView(_ uiView: WKWebView, context: Context) { uiView.isHidden = !show // uiView.scrollView.contentInset = .zero // uiView.scrollView.scrollIndicatorInsets = .zero } public func makeCoordinator() -> Coordinator { Coordinator(self) } public func setInitialProperties() { container.setInitialProperties( editingNote: editingNote, codeEditorTheme: colorScheme == .dark ? editorThemeDark : editorThemeLight, editorKeymap: editorKeymap, theme: theme, fontSize: webviewFontSize, editorThemeDark: editorThemeDark, editorThemeLight: editorThemeLight, darkMode: colorScheme == .dark, modelContext: modelContext ) } public func setInitialContent() { let s = editingNote.markdown.body.toQuotedJavascriptString() ?? "''" container.runJavascript( """ window.localStorage.setItem("\(SplitviewEditorWebviewLocalStorageKeys.initialValue.rawValue)", \(s)) window.setEditorContent(\(s)) """ ) } public func handleViewNoteByUserDefinedId(id: String) { print("Here...") let fetchDescriptor = FetchDescriptor<NoteModel>( predicate: #Predicate { note in note.frontMatter.userDefinedId == id }) if let notes = try? self.modelContext.fetch(fetchDescriptor) { if !notes.isEmpty { let note = notes.first self.editingNote = note! self.onNavigateToNote(note!) } } } public func handleTagClick(tagBody: String) { let fetchDescriptor = FetchDescriptor<TagModel>( predicate: #Predicate<TagModel> { t in t.value == tagBody }) if let tags = try? modelContext.fetch(fetchDescriptor) { if !tags.isEmpty { fullScreenCover = .tagSearch(tag: tags.first!) } } } } public struct MdxEditorWebview: View { @State private var show: Bool = false @State private var showEditNoteTaggables: Bool = false @Environment(ThemeManager.self) private var themeManager: ThemeManager let url: URL @Binding var theme: WebViewTheme @Binding var editorThemeDark: CodeSyntaxTheme @Binding var editorThemeLight: CodeSyntaxTheme @Binding var editingNote: NoteModel @Binding var editorKeymap: EditorKeymap @Binding var fullScreenCover: MainFullScreenCover? var onNavigateToNote: (NoteModel) -> Void let container: MdxEditorWebviewContainer public init( url: URL, theme: Binding<WebViewTheme>, editorThemeDark: Binding<CodeSyntaxTheme>, editorThemeLight: Binding<CodeSyntaxTheme>, editingNote: Binding<NoteModel>, editorKeymap: Binding<EditorKeymap>, container: MdxEditorWebviewContainer, onNavigateToNote: @escaping (NoteModel) -> Void, fullScreenCover: Binding<MainFullScreenCover?>? ) { self.url = url self._theme = theme self._editorThemeDark = editorThemeDark self._editorThemeLight = editorThemeLight self._editingNote = editingNote self._editorKeymap = editorKeymap self.container = container self.onNavigateToNote = onNavigateToNote if let fs = fullScreenCover { self._fullScreenCover = fs } else { self._fullScreenCover = .constant(nil) } self.onNavigateToNote = onNavigateToNote } public var body: some View { ZStack(alignment: show ? .bottomTrailing : .center) { MdxEditorWebviewInternal( url: url, theme: $theme, editorThemeDark: $editorThemeDark, editorThemeLight: $editorThemeLight, editingNote: $editingNote, editorKeymap: $editorKeymap, container: container, show: $show, onNavigateToNote: onNavigateToNote, fullScreenCover: $fullScreenCover, ) .disableAnimations() .frame( alignment: .bottom ) .scrollDisabled(true) if !show { ProgressView() .progressViewStyle(.circular) .scaleEffect(1.5) .tint(themeManager.theme.primary) } else { FloatingButtonView( buttons: [ FloatingButtonItem( id: "addTaggable", systemImage: "tag.fill", action: { withAnimation { showEditNoteTaggables.toggle() } } ), FloatingButtonItem( id: "toggleBookmarked", systemImage: editingNote.bookmarked ? "bookmark.fill" : "bookmark", action: { editingNote.bookmarked.toggle() } ) ] ) .padding() } } .fullScreenCover( isPresented: $showEditNoteTaggables, content: { EditNoteTaggablesView( editingNote: $editingNote, open: $showEditNoteTaggables ) }, ) } func onLoad() async { } } #endif

Thank you in advance for any suggestions. I've only been working with Swift for a couple months so there's still a lot for me to learn.

Read Entire Article