diff --git a/Promptly.xcodeproj/project.pbxproj b/Promptly.xcodeproj/project.pbxproj
index 1fd713a..5ab0761 100644
--- a/Promptly.xcodeproj/project.pbxproj
+++ b/Promptly.xcodeproj/project.pbxproj
@@ -419,7 +419,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Promptly/Promptly.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 20;
+ CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 8Y3J97SYZG;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
@@ -444,7 +444,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.5;
- MARKETING_VERSION = 1.0.4;
+ MARKETING_VERSION = 1.0.5;
PRODUCT_BUNDLE_IDENTIFIER = com.urbanmechanicsltd.Promptly;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -464,7 +464,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Promptly/Promptly.entitlements;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 20;
+ CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_TEAM = 8Y3J97SYZG;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
@@ -489,7 +489,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.5;
- MARKETING_VERSION = 1.0.4;
+ MARKETING_VERSION = 1.0.5;
PRODUCT_BUNDLE_IDENTIFIER = com.urbanmechanicsltd.Promptly;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
diff --git a/Promptly.xcodeproj/project.xcworkspace/xcuserdata/sashabagrov.xcuserdatad/UserInterfaceState.xcuserstate b/Promptly.xcodeproj/project.xcworkspace/xcuserdata/sashabagrov.xcuserdatad/UserInterfaceState.xcuserstate
index 48a8dec..ca8c7b9 100644
Binary files a/Promptly.xcodeproj/project.xcworkspace/xcuserdata/sashabagrov.xcuserdatad/UserInterfaceState.xcuserstate and b/Promptly.xcodeproj/project.xcworkspace/xcuserdata/sashabagrov.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/Promptly.xcodeproj/xcuserdata/sashabagrov.xcuserdatad/xcschemes/xcschememanagement.plist b/Promptly.xcodeproj/xcuserdata/sashabagrov.xcuserdatad/xcschemes/xcschememanagement.plist
index ac39bd4..4130238 100644
--- a/Promptly.xcodeproj/xcuserdata/sashabagrov.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Promptly.xcodeproj/xcuserdata/sashabagrov.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,12 +7,12 @@
Promptly-WatchOS Watch App.xcscheme_^#shared#^_
orderHint
- 1
+ 0
Promptly.xcscheme_^#shared#^_
orderHint
- 0
+ 1
diff --git a/Promptly/PromptlyApp.swift b/Promptly/PromptlyApp.swift
index 4795a30..42ff366 100644
--- a/Promptly/PromptlyApp.swift
+++ b/Promptly/PromptlyApp.swift
@@ -53,25 +53,9 @@ extension PromptlyApp: WhatsNewCollectionProvider {
/// A WhatsNewCollection
var whatsNewCollection: WhatsNewCollection {
WhatsNew(
- version: "1.0.4",
+ version: "1.0.5",
title: "DSMPrompt",
features: [
- .init(
- image: .init(
- systemName: "square.and.arrow.down.on.square",
- foregroundColor: .orange
- ),
- title: "Import & Export Show Files",
- subtitle: "Import and export show files to have manual backups / share between members."
- ),
- .init(
- image: .init(
- systemName: "wand.and.stars",
- foregroundColor: .cyan
- ),
- title: "What's New View",
- subtitle: "Find out what's changed between versions."
- ),
.init(
image: .init(
systemName: "hammer",
@@ -79,14 +63,6 @@ extension PromptlyApp: WhatsNewCollectionProvider {
),
title: "Bug Fixes",
subtitle: "Bug fixes and stability improvements."
- ),
- .init(
- image: .init(
- systemName: "hammer",
- foregroundColor: .red
- ),
- title: "PDF Rendering",
- subtitle: "Fixed bug where if you were in dark mode the text would not show in PDF exports."
)
],
primaryAction: .init(
diff --git a/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift b/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift
index e93eaff..522af2d 100644
--- a/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift
+++ b/Promptly/Views/Scripts/Edit Contents/EditScriptView.swift
@@ -48,6 +48,9 @@ struct EditScriptView: View {
@State private var lineBeingFlagged: ScriptLine?
@StateObject private var refreshTrigger = RefreshTrigger()
+ @State private var lineGroups: [LineGroup] = []
+
+
var sortedLines: [ScriptLine] {
script.lines.sorted { $0.lineNumber < $1.lineNumber }
}
@@ -126,7 +129,7 @@ struct EditScriptView: View {
}
}
.sheet(isPresented: $showingFlagEditor) {
- if selectedLines.count == 1, let line = lineBeingFlagged {
+ if selectedLines.count == 1, let line = selectedLinesArray.first {
FlagEditorView(line: line) {
try? modelContext.save()
refreshTrigger.refresh()
@@ -195,6 +198,9 @@ struct EditScriptView: View {
}
.navigationTitle(Text(script.name))
.navigationBarTitleDisplayMode(.inline)
+ .task {
+ lineGroups = await groupLinesBySection(lines: sortedLines, sections: sortedSections)
+ }
}
// MARK: - View Components
@@ -287,95 +293,127 @@ struct EditScriptView: View {
}
private var scriptContentView: some View {
- ScrollView {
- LazyVStack(spacing: 8) {
- ForEach(groupLinesBySection(), id: \.id) { group in
- if let section = group.section {
- SectionHeaderView(section: section)
- .padding(.horizontal)
- .padding(.top, 8)
- }
-
- ForEach(group.lines, id: \.id) { line in
- EditableScriptLineView(
- line: line,
- isEditing: isEditing,
- isSelected: selectedLines.contains(line.id),
- isEditingText: editingLineId == line.id,
- isSelectingForSection: isSelectingLineForSection,
- editingText: $editingText
- ) { selectedLine in
- if isSelectingLineForSection {
- selectedLineForSection = selectedLine
- showingLineConfirmation = true
- isSelectingLineForSection = false
- } else {
- toggleLineSelection(selectedLine)
- }
- } onStartTextEdit: { lineToEdit in
- startEditing(line: lineToEdit)
- } onFinishTextEdit: { newText in
- finishEditing(newText: newText)
- } onInsertAfter: { lineToInsertAfter in
- insertAfterLineNumber = lineToInsertAfter.lineNumber
- showingAddLine = true
- } onEditFlags: { lineToFlag in
- lineBeingFlagged = lineToFlag
- showingFlagEditor = true
- }
- }
+ ScriptTableView(
+ lineGroups: lineGroups,
+ isEditing: isEditing,
+ selectedLines: selectedLines,
+ editingLineId: editingLineId,
+ isSelectingForSection: isSelectingLineForSection,
+ editingText: $editingText,
+ onToggleSelection: { selectedLine in
+ if isSelectingLineForSection {
+ selectedLineForSection = selectedLine
+ showingLineConfirmation = true
+ isSelectingLineForSection = false
+ } else {
+ toggleLineSelection(selectedLine)
}
+ },
+ onStartTextEdit: { lineToEdit in
+ startEditing(line: lineToEdit)
+ },
+ onFinishTextEdit: { newText in
+ finishEditing(newText: newText)
+ },
+ onInsertAfter: { lineToInsertAfter in
+ insertAfterLineNumber = lineToInsertAfter.lineNumber
+ showingAddLine = true
+ },
+ onEditFlags: { lineToFlag in
+ lineBeingFlagged = lineToFlag
+ showingFlagEditor = true
+ },
+ onSectionTap: { section in
+ print("sec tap")
+ // dismiss()
+ // NotificationCenter.default.post(
+ // name: NSNotification.Name("ScrollToSection"),
+ // object: section.id
+ // )
}
- .padding()
- }
+ )
}
// MARK: - Actions
- private func groupLinesBySection() -> [LineGroup] {
- // For performance, only process visible sections
- let maxVisibleSections = 20
- let limitedSections = Array(sortedSections.prefix(maxVisibleSections))
+ private func groupLinesBySection(lines: [ScriptLine], sections: [ScriptSection]) async -> [LineGroup] {
+ guard !lines.isEmpty else { return [] }
+ guard !sections.isEmpty else {
+ return [LineGroup(section: nil, lines: lines)]
+ }
+
+ let sectionRanges = await withTaskGroup(of: (Int, Int, Int).self) { group in
+ for (index, section) in sections.enumerated() {
+ group.addTask {
+ let startLine = section.startLineNumber
+ let endLine = (index + 1 < sections.count) ?
+ sections[index + 1].startLineNumber - 1 :
+ lines.last?.lineNumber ?? startLine
+ return (index, startLine, endLine)
+ }
+ }
+
+ var ranges: [(Int, Int, Int)] = []
+ for await range in group {
+ ranges.append(range)
+ }
+ return ranges.sorted { $0.0 < $1.0 }
+ }
var groups: [LineGroup] = []
- var lastProcessedLine = 0
+ var processedLines = Set()
- for section in limitedSections {
- let startLine = section.startLineNumber
+ for (index, startLine, endLine) in sectionRanges {
+ let section = sections[index]
- // Find the next section's start line (or end of script)
- let nextSectionStart = sortedSections.first {
- $0.startLineNumber > startLine
- }?.startLineNumber ?? (sortedLines.last?.lineNumber ?? 0) + 1
+ let startIdx = binarySearchStart(lines: lines, lineNumber: startLine)
+ let endIdx = binarySearchEnd(lines: lines, lineNumber: endLine)
- // Add ungrouped lines before this section
- if startLine > lastProcessedLine + 1 {
- let ungroupedLines = sortedLines.filter {
- $0.lineNumber > lastProcessedLine && $0.lineNumber < startLine
- }
- if !ungroupedLines.isEmpty {
- groups.append(LineGroup(section: nil, lines: ungroupedLines))
- }
- }
+ guard startIdx < lines.count && endIdx >= 0 else { continue }
- // Add this section's lines (only up to next section)
- let sectionLines = sortedLines.filter {
- $0.lineNumber >= startLine && $0.lineNumber < nextSectionStart
- }
+ let sectionLines = lines[startIdx...min(endIdx, lines.count - 1)]
+ .filter { !processedLines.contains($0.lineNumber) }
if !sectionLines.isEmpty {
- groups.append(LineGroup(section: section, lines: sectionLines))
- lastProcessedLine = sectionLines.last?.lineNumber ?? lastProcessedLine
+ groups.append(LineGroup(section: section, lines: Array(sectionLines)))
+ processedLines.formUnion(sectionLines.map { $0.lineNumber })
}
}
- // Add remaining ungrouped lines
- let remainingLines = sortedLines.filter { $0.lineNumber > lastProcessedLine }
- if !remainingLines.isEmpty {
- groups.append(LineGroup(section: nil, lines: remainingLines))
+ let ungroupedLines = lines.filter { !processedLines.contains($0.lineNumber) }
+ if !ungroupedLines.isEmpty {
+ groups.append(LineGroup(section: nil, lines: ungroupedLines))
}
- return groups
+ return groups.sorted {
+ ($0.lines.first?.lineNumber ?? 0) < ($1.lines.first?.lineNumber ?? 0)
+ }
+ }
+
+ private func binarySearchStart(lines: [ScriptLine], lineNumber: Int) -> Int {
+ var left = 0, right = lines.count - 1
+ while left <= right {
+ let mid = (left + right) / 2
+ if lines[mid].lineNumber >= lineNumber {
+ right = mid - 1
+ } else {
+ left = mid + 1
+ }
+ }
+ return left
+ }
+
+ private func binarySearchEnd(lines: [ScriptLine], lineNumber: Int) -> Int {
+ var left = 0, right = lines.count - 1
+ while left <= right {
+ let mid = (left + right) / 2
+ if lines[mid].lineNumber <= lineNumber {
+ left = mid + 1
+ } else {
+ right = mid - 1
+ }
+ }
+ return right
}
private func updateSectionsAfterLineChange(at changePoint: Int, delta: Int) {
@@ -667,7 +705,7 @@ struct EditableScriptLineView: View {
private var backgroundOpacity: Double {
if isSelected && isEditing {
- return 0.6
+ return 1.0
} else if line.isMarked {
return 0.3
} else {
diff --git a/Promptly/Views/Scripts/ScriptEditorView.swift b/Promptly/Views/Scripts/ScriptEditorView.swift
index 95a0f94..4e849f0 100644
--- a/Promptly/Views/Scripts/ScriptEditorView.swift
+++ b/Promptly/Views/Scripts/ScriptEditorView.swift
@@ -180,41 +180,26 @@ struct ScriptEditorView: View {
}
private var scriptContentView: some View {
- ScrollViewReader { proxy in
- ScrollView {
- LazyVStack(alignment: .leading, spacing: 12) {
- ForEach(lineGroups, id: \.id) { group in
- if let section = group.section {
- SectionHeaderView(section: section)
- .padding(.horizontal, 16)
- .padding(.top, 8)
- }
-
- ForEach(group.lines, id: \.id) { line in
- ScriptLineView(
- line: line,
- isSelected: selectedLine?.id == line.id,
- isEditing: editingLineId == line.id,
- editingText: $editingText,
- onElementTap: { element in
- handleElementTap(element: element, line: line)
- },
- onLineTap: {
- handleLineTap(line: line)
- },
- onEditComplete: { newText in
- updateLineContent(line: line, newText: newText)
- },
- onCueDelete: handleCueDelete,
- onCueEdit: handleCueEdit
- )
- .id("line-\(line.id)")
- }
- }
- }
- .padding()
+ ScriptTableView(
+ lineGroups: lineGroups,
+ selectedLine: selectedLine,
+ editingLineId: editingLineId,
+ editingText: $editingText,
+ onElementTap: { element, line in
+ handleElementTap(element: element, line: line)
+ },
+ onLineTap: { line in
+ handleLineTap(line: line)
+ },
+ onEditComplete: { line, newText in
+ updateLineContent(line: line, newText: newText)
+ },
+ onCueDelete: handleCueDelete,
+ onCueEdit: handleCueEdit,
+ onSectionTap: { section in
+
}
- }
+ )
}
private func handleElementTap(element: LineElement, line: ScriptLine) {
diff --git a/Promptly/Views/Scripts/UIKit Views/ScriptTableView.swift b/Promptly/Views/Scripts/UIKit Views/ScriptTableView.swift
new file mode 100644
index 0000000..b35a195
--- /dev/null
+++ b/Promptly/Views/Scripts/UIKit Views/ScriptTableView.swift
@@ -0,0 +1,416 @@
+//
+// ScriptTableView.swift
+// Promptly
+//
+// Created by Sasha Bagrov on 27/10/2025.
+//
+
+import Foundation
+import UIKit
+import SwiftUI
+
+struct ScriptTableView: UIViewRepresentable {
+ let lineGroups: [LineGroup]
+
+ // Edit mode properties
+ let isEditing: Bool
+ let selectedLines: Set
+ let editingLineId: UUID?
+ let isSelectingForSection: Bool
+ @Binding var editingText: String
+
+ // Edit mode callbacks
+ let onToggleSelection: ((ScriptLine) -> Void)?
+ let onStartTextEdit: ((ScriptLine) -> Void)?
+ let onFinishTextEdit: ((String) -> Void)?
+ let onInsertAfter: ((ScriptLine) -> Void)?
+ let onEditFlags: ((ScriptLine) -> Void)?
+ let onSectionTap: ((ScriptSection) -> Void)?
+
+ // Reading mode properties
+ let selectedLine: ScriptLine?
+
+ // Reading mode callbacks
+ let onElementTap: ((LineElement, ScriptLine) -> Void)?
+ let onLineTap: ((ScriptLine) -> Void)?
+ let onEditComplete: ((ScriptLine, String) -> Void)?
+ let onCueDelete: ((Cue) -> Void)?
+ let onCueEdit: ((Cue) -> Void)?
+
+ private let mode: TableMode
+
+ enum TableMode {
+ case editing, reading
+ }
+
+ // MARK: - Dual Initialisers
+
+ // Edit mode initialiser (unchanged)
+ init(
+ lineGroups: [LineGroup],
+ isEditing: Bool,
+ selectedLines: Set,
+ editingLineId: UUID?,
+ isSelectingForSection: Bool,
+ editingText: Binding,
+ onToggleSelection: @escaping (ScriptLine) -> Void,
+ onStartTextEdit: @escaping (ScriptLine) -> Void,
+ onFinishTextEdit: @escaping (String) -> Void,
+ onInsertAfter: @escaping (ScriptLine) -> Void,
+ onEditFlags: @escaping (ScriptLine) -> Void,
+ onSectionTap: @escaping (ScriptSection) -> Void
+ ) {
+ self.lineGroups = lineGroups
+ self.isEditing = isEditing
+ self.selectedLines = selectedLines
+ self.editingLineId = editingLineId
+ self.isSelectingForSection = isSelectingForSection
+ self._editingText = editingText
+ self.onToggleSelection = onToggleSelection
+ self.onStartTextEdit = onStartTextEdit
+ self.onFinishTextEdit = onFinishTextEdit
+ self.onInsertAfter = onInsertAfter
+ self.onEditFlags = onEditFlags
+ self.onSectionTap = onSectionTap
+
+ // Reading mode defaults
+ self.selectedLine = nil
+ self.onElementTap = nil
+ self.onLineTap = nil
+ self.onEditComplete = nil
+ self.onCueDelete = nil
+ self.onCueEdit = nil
+
+ self.mode = .editing
+ }
+
+ // Reading mode initialiser
+ init(
+ lineGroups: [LineGroup],
+ selectedLine: ScriptLine?,
+ editingLineId: UUID?,
+ editingText: Binding,
+ onElementTap: @escaping (LineElement, ScriptLine) -> Void,
+ onLineTap: @escaping (ScriptLine) -> Void,
+ onEditComplete: @escaping (ScriptLine, String) -> Void,
+ onCueDelete: @escaping (Cue) -> Void,
+ onCueEdit: @escaping (Cue) -> Void,
+ onSectionTap: @escaping (ScriptSection) -> Void
+ ) {
+ self.lineGroups = lineGroups
+ self.selectedLine = selectedLine
+ self.editingLineId = editingLineId
+ self._editingText = editingText
+ self.onElementTap = onElementTap
+ self.onLineTap = onLineTap
+ self.onEditComplete = onEditComplete
+ self.onCueDelete = onCueDelete
+ self.onCueEdit = onCueEdit
+ self.onSectionTap = onSectionTap
+
+ // Edit mode defaults
+ self.isEditing = false
+ self.selectedLines = []
+ self.isSelectingForSection = false
+ self.onToggleSelection = nil
+ self.onStartTextEdit = nil
+ self.onFinishTextEdit = nil
+ self.onInsertAfter = nil
+ self.onEditFlags = nil
+
+ self.mode = .reading
+ }
+
+ func makeUIView(context: Context) -> UITableView {
+ let tableView = UITableView()
+ tableView.delegate = context.coordinator
+ tableView.dataSource = context.coordinator
+ tableView.separatorStyle = .none
+ tableView.backgroundColor = UIColor.systemGroupedBackground
+ tableView.showsVerticalScrollIndicator = true
+ tableView.contentInset = UIEdgeInsets(top: 16, left: 0, bottom: 16, right: 0)
+
+ // Register cells for both modes
+ tableView.register(HostingLineCell.self, forCellReuseIdentifier: "LineCell")
+ tableView.register(HostingReadingLineCell.self, forCellReuseIdentifier: "ReadingLineCell")
+ tableView.register(HostingSectionCell.self, forCellReuseIdentifier: "SectionCell")
+
+ return tableView
+ }
+
+ func updateUIView(_ uiView: UITableView, context: Context) {
+ context.coordinator.parent = self
+ uiView.reloadData()
+ }
+
+ func makeCoordinator() -> Coordinator {
+ Coordinator(self)
+ }
+
+ class Coordinator: NSObject, UITableViewDataSource, UITableViewDelegate {
+ var parent: ScriptTableView
+
+ init(_ parent: ScriptTableView) {
+ self.parent = parent
+ }
+
+ private var flattenedItems: [(type: ItemType, group: LineGroup, line: ScriptLine?)] {
+ var items: [(ItemType, LineGroup, ScriptLine?)] = []
+
+ for group in parent.lineGroups {
+ if let section = group.section {
+ items.append((.section, group, nil))
+ }
+ for line in group.lines {
+ items.append((.line, group, line))
+ }
+ }
+ return items
+ }
+
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return flattenedItems.count
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let item = flattenedItems[indexPath.row]
+
+ switch item.type {
+ case .section:
+ let cell = tableView.dequeueReusableCell(withIdentifier: "SectionCell", for: indexPath) as! HostingSectionCell
+ cell.configure(with: item.group.section!, onTap: parent.onSectionTap!)
+ return cell
+
+ case .line:
+ switch parent.mode {
+ case .editing:
+ let cell = tableView.dequeueReusableCell(withIdentifier: "LineCell", for: indexPath) as! HostingLineCell
+ cell.configure(
+ line: item.line!,
+ isEditing: parent.isEditing,
+ isSelected: parent.selectedLines.contains(item.line!.id),
+ isEditingText: parent.editingLineId == item.line!.id,
+ isSelectingForSection: parent.isSelectingForSection,
+ editingText: parent.editingText,
+ onToggleSelection: parent.onToggleSelection!,
+ onStartTextEdit: parent.onStartTextEdit!,
+ onFinishTextEdit: parent.onFinishTextEdit!,
+ onInsertAfter: parent.onInsertAfter!,
+ onEditFlags: parent.onEditFlags!
+ )
+ return cell
+
+ case .reading:
+ let cell = tableView.dequeueReusableCell(withIdentifier: "ReadingLineCell", for: indexPath) as! HostingReadingLineCell
+ cell.configure(
+ line: item.line!,
+ isSelected: parent.selectedLine?.id == item.line!.id,
+ isEditing: parent.editingLineId == item.line!.id,
+ editingText: parent.editingText,
+ onElementTap: { element in
+ parent.onElementTap?(element, item.line!)
+ },
+ onLineTap: {
+ parent.onLineTap?(item.line!)
+ },
+ onEditComplete: { newText in
+ parent.onEditComplete?(item.line!, newText)
+ },
+ onCueDelete: parent.onCueDelete!,
+ onCueEdit: parent.onCueEdit!
+ )
+ return cell
+ }
+ }
+ }
+
+ func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
+ let item = flattenedItems[indexPath.row]
+ return item.type == .section ? 80 : 60
+ }
+
+ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+ return UITableView.automaticDimension
+ }
+ }
+
+ enum ItemType {
+ case section, line
+ }
+}
+
+// UITableViewCell that hosts SwiftUI line content
+class HostingLineCell: UITableViewCell {
+ private var hostingController: UIHostingController?
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ // Clean up when cell is reused
+ hostingController?.view.removeFromSuperview()
+ hostingController = nil
+ }
+
+ func configure(
+ line: ScriptLine,
+ isEditing: Bool,
+ isSelected: Bool,
+ isEditingText: Bool,
+ isSelectingForSection: Bool,
+ editingText: String,
+ onToggleSelection: @escaping (ScriptLine) -> Void,
+ onStartTextEdit: @escaping (ScriptLine) -> Void,
+ onFinishTextEdit: @escaping (String) -> Void,
+ onInsertAfter: @escaping (ScriptLine) -> Void,
+ onEditFlags: @escaping (ScriptLine) -> Void
+ ) {
+ // Create binding for editingText
+ let editingTextBinding = Binding(
+ get: { editingText },
+ set: { _ in } // Handle in the callbacks
+ )
+
+ let swiftUIView = AnyView(
+ EditableScriptLineView(
+ line: line,
+ isEditing: isEditing,
+ isSelected: isSelected,
+ isEditingText: isEditingText,
+ isSelectingForSection: isSelectingForSection,
+ editingText: editingTextBinding,
+ onToggleSelection: onToggleSelection,
+ onStartTextEdit: onStartTextEdit,
+ onFinishTextEdit: onFinishTextEdit,
+ onInsertAfter: onInsertAfter,
+ onEditFlags: onEditFlags
+ )
+ .padding(.horizontal, 16)
+ .padding(.vertical, 4)
+ )
+
+ if let hostingController = hostingController {
+ hostingController.rootView = swiftUIView
+ } else {
+ hostingController = UIHostingController(rootView: swiftUIView)
+ hostingController!.view.backgroundColor = .clear
+ hostingController!.view.translatesAutoresizingMaskIntoConstraints = false
+
+ contentView.addSubview(hostingController!.view)
+ NSLayoutConstraint.activate([
+ hostingController!.view.topAnchor.constraint(equalTo: contentView.topAnchor),
+ hostingController!.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+ hostingController!.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+ hostingController!.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
+ ])
+ }
+
+ // Ensure proper selection state
+ selectionStyle = .none
+ backgroundColor = .clear
+ }
+}
+
+// UITableViewCell that hosts SwiftUI section content
+class HostingSectionCell: UITableViewCell {
+ private var hostingController: UIHostingController?
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ // Clean up when cell is reused
+ hostingController?.view.removeFromSuperview()
+ hostingController = nil
+ }
+
+ func configure(with section: ScriptSection, onTap: @escaping (ScriptSection) -> Void) {
+ let swiftUIView = AnyView(
+ Button(action: { onTap(section) }) {
+ SectionHeaderView(section: section)
+ }
+ .buttonStyle(PlainButtonStyle())
+ .padding(.horizontal, 16)
+ .padding(.top, 8)
+ )
+
+ if let hostingController = hostingController {
+ hostingController.rootView = swiftUIView
+ } else {
+ hostingController = UIHostingController(rootView: swiftUIView)
+ hostingController!.view.backgroundColor = .clear
+ hostingController!.view.translatesAutoresizingMaskIntoConstraints = false
+
+ contentView.addSubview(hostingController!.view)
+ NSLayoutConstraint.activate([
+ hostingController!.view.topAnchor.constraint(equalTo: contentView.topAnchor),
+ hostingController!.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+ hostingController!.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+ hostingController!.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
+ ])
+ }
+
+ // Ensure proper selection state
+ selectionStyle = .none
+ backgroundColor = .clear
+ }
+}
+
+
+class HostingReadingLineCell: UITableViewCell {
+ private var hostingController: UIHostingController?
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ hostingController?.view.removeFromSuperview()
+ hostingController = nil
+ }
+
+ func configure(
+ line: ScriptLine,
+ isSelected: Bool,
+ isEditing: Bool,
+ editingText: String,
+ onElementTap: @escaping (LineElement) -> Void,
+ onLineTap: @escaping () -> Void,
+ onEditComplete: @escaping (String) -> Void,
+ onCueDelete: @escaping (Cue) -> Void,
+ onCueEdit: @escaping (Cue) -> Void
+ ) {
+ let editingTextBinding = Binding(
+ get: { editingText },
+ set: { _ in }
+ )
+
+ let swiftUIView = AnyView(
+ ScriptLineView(
+ line: line,
+ isSelected: isSelected,
+ isEditing: isEditing,
+ editingText: editingTextBinding,
+ onElementTap: onElementTap,
+ onLineTap: onLineTap,
+ onEditComplete: onEditComplete,
+ onCueDelete: onCueDelete,
+ onCueEdit: onCueEdit
+ )
+ .padding(.horizontal, 16)
+ .padding(.vertical, 4)
+ )
+
+ if let hostingController = hostingController {
+ hostingController.rootView = swiftUIView
+ } else {
+ hostingController = UIHostingController(rootView: swiftUIView)
+ hostingController!.view.backgroundColor = .clear
+ hostingController!.view.translatesAutoresizingMaskIntoConstraints = false
+
+ contentView.addSubview(hostingController!.view)
+ NSLayoutConstraint.activate([
+ hostingController!.view.topAnchor.constraint(equalTo: contentView.topAnchor),
+ hostingController!.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
+ hostingController!.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
+ hostingController!.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
+ ])
+ }
+
+ selectionStyle = .none
+ backgroundColor = .clear
+ }
+}