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.
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, editorContainer: MdxEditorWebviewContainer,
onNavigateToNote: ((NoteModel) -> Void)?,
fullScreenCover: Binding
) {
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,
editorThemeDark: Binding,
editorThemeLight: Binding,
editingNote: Binding,
editorKeymap: Binding,
container: MdxEditorWebviewContainer,
show: Binding,
onNavigateToNote: @escaping (NoteModel) -> Void,
fullScreenCover: Binding
) {
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(
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(
predicate: #Predicate { 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,
editorThemeDark: Binding,
editorThemeLight: Binding,
editingNote: Binding,
editorKeymap: Binding,
container: MdxEditorWebviewContainer,
onNavigateToNote: @escaping (NoteModel) -> Void,
fullScreenCover: Binding?
) {
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.
