Add path-filtered repository history

This commit is contained in:
Georg Bauer
2026-07-31 15:43:07 +02:00
parent c4e64b5c0c
commit 0583d05170
7 changed files with 360 additions and 50 deletions

View File

@@ -207,6 +207,11 @@ xcrun simctl launch booted de.rfc1437.gotcha
- [ ] In Files, traverse several nested folders using rows, the navigation-bar - [ ] In Files, traverse several nested folders using rows, the navigation-bar
Back button, and the left-edge swipe. Folder contents and titles match the Back button, and the left-edge swipe. Folder contents and titles match the
repository hierarchy. repository hierarchy.
- [ ] In the repository root and in several nested folders, switch between
**Files** and **History**. Each folder history contains only commits that
affect that folder or its descendants. Switch back to Files and verify the
same folder and navigation stack are preserved. Open a history commit and
verify its changed-files detail opens normally.
- [ ] Open representative source files in several languages, including Rust, - [ ] Open representative source files in several languages, including Rust,
Swift, and a scripting or markup language. Keywords, strings, comments, Swift, and a scripting or markup language. Keywords, strings, comments,
types, and punctuation use plausible language-specific highlighting. types, and punctuation use plausible language-specific highlighting.
@@ -223,6 +228,11 @@ xcrun simctl launch booted de.rfc1437.gotcha
is syntax-highlighted, selectable, does not word-wrap, and scrolls is syntax-highlighted, selectable, does not word-wrap, and scrolls
horizontally for long lines. Switch back and verify the rendered preview horizontally for long lines. Switch back and verify the rendered preview
is restored without stale or overlapping content. is restored without stale or overlapping content.
- [ ] Switch Markdown files among **Preview**, **Source**, and **History** and
switch other source and native-preview files between **Content** and
**History**. Each history contains only commits that affect that exact
file. Returning to content preserves the file path and restores its native
preview or source presentation without stale or overlapping views.
- [ ] Open representative image, PDF, audio, and video files. Each uses the - [ ] Open representative image, PDF, audio, and video files. Each uses the
native preview appropriate to its media type and returns cleanly to Files. native preview appropriate to its media type and returns cleanly to Files.

View File

@@ -450,9 +450,10 @@ pub async fn load_branch_commits(
owner: &str, owner: &str,
repository: &str, repository: &str,
branch: &str, branch: &str,
path: Option<&str>,
pages: u32, pages: u32,
) -> Result<Page<HistoryCommit>, String> { ) -> Result<Page<HistoryCommit>, String> {
load_commits_for_ref(server, owner, repository, Some(branch), pages) load_commits_for_ref(server, owner, repository, Some(branch), path, pages)
.await .await
.map(|page| Page { .map(|page| Page {
has_more: page.has_more, has_more: page.has_more,
@@ -476,15 +477,24 @@ pub async fn load_all_commits(
owner: &str, owner: &str,
repository: &str, repository: &str,
branches: &[String], branches: &[String],
path: Option<&str>,
pages: u32, pages: u32,
) -> Result<Page<HistoryCommit>, String> { ) -> Result<Page<HistoryCommit>, String> {
let mut tasks = JoinSet::new(); let mut tasks = JoinSet::new();
for (lane, branch) in branches.iter().cloned().enumerate() { for (lane, branch) in branches.iter().cloned().enumerate() {
let (server, owner, repository) = let (server, owner, repository) =
(server.clone(), owner.to_string(), repository.to_string()); (server.clone(), owner.to_string(), repository.to_string());
let path = path.map(str::to_string);
tasks.spawn(async move { tasks.spawn(async move {
let commits = let commits = load_commits_for_ref(
load_commits_for_ref(&server, &owner, &repository, Some(&branch), pages).await?; &server,
&owner,
&repository,
Some(&branch),
path.as_deref(),
pages,
)
.await?;
Ok::<_, String>((lane, branch, commits)) Ok::<_, String>((lane, branch, commits))
}); });
} }
@@ -1024,6 +1034,7 @@ async fn load_commits_for_ref(
owner: &str, owner: &str,
repository: &str, repository: &str,
branch: Option<&str>, branch: Option<&str>,
path: Option<&str>,
pages: u32, pages: u32,
) -> Result<Page<models::Commit>, String> { ) -> Result<Page<models::Commit>, String> {
let client = let client =
@@ -1037,7 +1048,7 @@ async fn load_commits_for_ref(
owner, owner,
repository, repository,
branch, branch,
None, path,
None, None,
None, None,
None, None,

View File

@@ -599,6 +599,7 @@ impl GotchaCore {
owner: String, owner: String,
repository: String, repository: String,
branch: Option<String>, branch: Option<String>,
path: String,
pages: u32, pages: u32,
) -> Result<CommitPage, GotchaError> { ) -> Result<CommitPage, GotchaError> {
let server = self.server()?; let server = self.server()?;
@@ -612,11 +613,16 @@ impl GotchaCore {
.map(|candidate| candidate.default_branch.clone()) .map(|candidate| candidate.default_branch.clone())
.unwrap_or_else(|| "main".into()); .unwrap_or_else(|| "main".into());
let branches = load_branches(&server, &owner, &repository, &default_branch).await?; let branches = load_branches(&server, &owner, &repository, &default_branch).await?;
let path = (!path.is_empty()).then_some(path.as_str());
let commits = match branch.as_deref() { let commits = match branch.as_deref() {
Some(branch) => { Some(branch) => {
load_branch_commits(&server, &owner, &repository, branch, pages.max(1)).await? load_branch_commits(&server, &owner, &repository, branch, path, pages.max(1))
.await?
}
None => {
load_all_commits(&server, &owner, &repository, &branches, path, pages.max(1))
.await?
} }
None => load_all_commits(&server, &owner, &repository, &branches, pages.max(1)).await?,
}; };
Ok(commit_page( Ok(commit_page(
branches, branches,

View File

@@ -613,7 +613,7 @@ public protocol GotchaCoreProtocol: AnyObject, Sendable {
func commitFiles(owner: String, repository: String, sha: String) async throws -> [FileRow] func commitFiles(owner: String, repository: String, sha: String) async throws -> [FileRow]
func commits(owner: String, repository: String, branch: String?, pages: UInt32) async throws -> CommitPage func commits(owner: String, repository: String, branch: String?, path: String, pages: UInt32) async throws -> CommitPage
func home(page: UInt32) async throws -> HomePage func home(page: UInt32) async throws -> HomePage
@@ -803,12 +803,12 @@ open func commitFiles(owner: String, repository: String, sha: String)async throw
) )
} }
open func commits(owner: String, repository: String, branch: String?, pages: UInt32)async throws -> CommitPage { open func commits(owner: String, repository: String, branch: String?, path: String, pages: UInt32)async throws -> CommitPage {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_gotcha_core_fn_method_gotchacore_commits( uniffi_gotcha_core_fn_method_gotchacore_commits(
self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterOptionString.lower(branch),FfiConverterUInt32.lower(pages) self.uniffiCloneHandle(),FfiConverterString.lower(owner),FfiConverterString.lower(repository),FfiConverterOptionString.lower(branch),FfiConverterString.lower(path),FfiConverterUInt32.lower(pages)
) )
}, },
pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer, pollFunc: ffi_gotcha_core_rust_future_poll_rust_buffer,
@@ -3948,7 +3948,7 @@ private let initializationResult: InitializationResult = {
if (uniffi_gotcha_core_checksum_method_gotchacore_commit_files() != 43926) { if (uniffi_gotcha_core_checksum_method_gotchacore_commit_files() != 43926) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_commits() != 59625) { if (uniffi_gotcha_core_checksum_method_gotchacore_commits() != 60062) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_gotcha_core_checksum_method_gotchacore_home() != 38990) { if (uniffi_gotcha_core_checksum_method_gotchacore_home() != 38990) {

View File

@@ -286,7 +286,7 @@ uint64_t uniffi_gotcha_core_fn_method_gotchacore_commit_files(uint64_t ptr, Rust
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
#define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS #define UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_COMMITS
uint64_t uniffi_gotcha_core_fn_method_gotchacore_commits(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer branch, uint32_t pages uint64_t uniffi_gotcha_core_fn_method_gotchacore_commits(uint64_t ptr, RustBuffer owner, RustBuffer repository, RustBuffer branch, RustBuffer path, uint32_t pages
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME #ifndef UNIFFI_FFIDEF_UNIFFI_GOTCHA_CORE_FN_METHOD_GOTCHACORE_HOME

View File

@@ -958,6 +958,7 @@ final class CommitsViewController: RefreshingTableViewController {
owner: owner, owner: owner,
repository: repository, repository: repository,
branch: branch, branch: branch,
path: "",
pages: requestedPage pages: requestedPage
) )
currentPage = requestedPage currentPage = requestedPage

View File

@@ -425,11 +425,17 @@ final class FilesViewController: RefreshingTableViewController {
@MainActor @MainActor
final class RepositoryDirectoryViewController: RefreshingTableViewController { final class RepositoryDirectoryViewController: RefreshingTableViewController {
private enum Mode: Int { case files, history }
private let context: AppContext private let context: AppContext
private let owner: String private let owner: String
private let repository: String private let repository: String
private let path: String private let path: String
private var mode = Mode.files
private var rows: [RepositoryContentRow] = [] private var rows: [RepositoryContentRow] = []
private var history: CommitPage?
private var currentPage: UInt32 = 0
private var loadedFiles = false
init(context: AppContext, owner: String, repository: String, path: String, name: String) { init(context: AppContext, owner: String, repository: String, path: String, name: String) {
self.context = context self.context = context
@@ -445,23 +451,47 @@ final class RepositoryDirectoryViewController: RefreshingTableViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
tableView.register(CommitCell.self, forCellReuseIdentifier: "commit")
navigationItem.prompt = title
let modeControl = UISegmentedControl(items: ["Files", "History"])
modeControl.selectedSegmentIndex = mode.rawValue
modeControl.addTarget(self, action: #selector(modeChanged(_:)), for: .valueChanged)
modeControl.accessibilityLabel = "Directory view"
navigationItem.titleView = modeControl
loadContent(refreshing: false) loadContent(refreshing: false)
} }
override func loadContent(refreshing: Bool) { override func loadContent(refreshing: Bool) {
switch mode {
case .files: loadFiles(refreshing: refreshing)
case .history: loadHistory(page: 1, refreshing: refreshing)
}
}
override func loadMoreContent() {
guard mode == .history else { return }
loadHistory(page: currentPage + 1, refreshing: false)
}
private func loadFiles(refreshing: Bool) {
resetPagination()
beginLoading(refreshing: refreshing) beginLoading(refreshing: refreshing)
loadingTask?.cancel() loadingTask?.cancel()
loadingTask = Task { loadingTask = Task {
do { do {
rows = try await context.core.repositoryContents( let rows = try await context.core.repositoryContents(
owner: owner, owner: owner,
repository: repository, repository: repository,
path: path path: path
) )
guard !Task.isCancelled, mode == .files else {
endLoading()
return
}
self.rows = rows
loadedFiles = true
tableView.reloadData() tableView.reloadData()
tableView.backgroundView = rows.isEmpty updateEmptyView()
? EmptyBackgroundView(title: "Empty folder", detail: "This folder does not contain any files.")
: nil
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled { show(error: error) }
} }
@@ -469,45 +499,258 @@ final class RepositoryDirectoryViewController: RefreshingTableViewController {
} }
} }
private func loadHistory(page requestedPage: UInt32, refreshing: Bool) {
if requestedPage == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel()
loadingTask = Task {
do {
let history = try await context.core.commits(
owner: owner,
repository: repository,
branch: nil,
path: path,
pages: requestedPage
)
guard !Task.isCancelled, mode == .history else {
if requestedPage == 1 { endLoading() }
return
}
self.history = history
currentPage = requestedPage
finishPagination(hasMore: history.hasMore)
tableView.reloadData()
updateEmptyView()
} catch {
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
if requestedPage == 1 { endLoading() }
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
rows.count mode == .files ? rows.count : history?.commits.count ?? 0
} }
override func tableView( override func tableView(
_ tableView: UITableView, _ tableView: UITableView,
cellForRowAt indexPath: IndexPath cellForRowAt indexPath: IndexPath
) -> UITableViewCell { ) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "repository-content") switch mode {
?? UITableViewCell(style: .subtitle, reuseIdentifier: "repository-content") case .files:
configureRepositoryContentCell(cell, row: rows[indexPath.row]) let cell = tableView.dequeueReusableCell(withIdentifier: "repository-content")
return cell ?? UITableViewCell(style: .subtitle, reuseIdentifier: "repository-content")
configureRepositoryContentCell(cell, row: rows[indexPath.row])
return cell
case .history:
let cell = tableView.dequeueReusableCell(withIdentifier: "commit", for: indexPath) as! CommitCell
if let history {
cell.configure(history.commits[indexPath.row], laneCount: history.laneCount)
}
return cell
}
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
mode == .history ? 86 : UITableView.automaticDimension
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true) tableView.deselectRow(at: indexPath, animated: true)
showRepositoryContent( switch mode {
rows[indexPath.row], case .files:
context: context, showRepositoryContent(
owner: owner, rows[indexPath.row],
repository: repository, context: context,
navigationController: navigationController owner: owner,
repository: repository,
navigationController: navigationController
)
case .history:
guard let commit = history?.commits[indexPath.row] else { return }
navigationController?.pushViewController(
FilesViewController(
context: context,
owner: owner,
repository: repository,
sha: commit.sha
),
animated: true
)
}
}
@objc private func modeChanged(_ sender: UISegmentedControl) {
guard let mode = Mode(rawValue: sender.selectedSegmentIndex), mode != self.mode else { return }
loadingTask?.cancel()
endLoading()
self.mode = mode
resetPagination()
tableView.backgroundView = nil
tableView.reloadData()
switch mode {
case .files:
if loadedFiles { updateEmptyView() } else { loadFiles(refreshing: false) }
case .history:
if let history {
finishPagination(hasMore: history.hasMore)
updateEmptyView()
} else {
loadHistory(page: 1, refreshing: false)
}
}
}
private func updateEmptyView() {
switch mode {
case .files:
tableView.backgroundView = loadedFiles && rows.isEmpty
? EmptyBackgroundView(title: "Empty folder", detail: "This folder does not contain any files.")
: nil
case .history:
tableView.backgroundView = history?.commits.isEmpty == true
? EmptyBackgroundView(title: "No commits", detail: "No commits affect this folder.")
: nil
}
}
}
@MainActor
final class RepositoryHistoryViewController: RefreshingTableViewController {
private let context: AppContext
private let owner: String
private let repository: String
private let path: String
private let emptyDetail: String
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
private var page: CommitPage?
private var currentPage: UInt32 = 0
init(
context: AppContext,
owner: String,
repository: String,
path: String,
emptyDetail: String
) {
self.context = context
self.owner = owner
self.repository = repository
self.path = path
self.emptyDetail = emptyDetail
super.init()
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(CommitCell.self, forCellReuseIdentifier: "commit")
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
loadPage(currentPage + 1, refreshing: false)
}
private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
if requestedPage == 1 {
resetPagination()
if !refreshing {
loadingIndicator.startAnimating()
tableView.backgroundView = loadingIndicator
}
}
loadingTask?.cancel()
loadingTask = Task {
do {
let page = try await context.core.commits(
owner: owner,
repository: repository,
branch: nil,
path: path,
pages: requestedPage
)
guard !Task.isCancelled else { return }
self.page = page
currentPage = requestedPage
finishPagination(hasMore: page.hasMore)
tableView.reloadData()
updateEmptyView()
} catch {
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
if requestedPage == 1 {
loadingIndicator.stopAnimating()
refreshControl?.endRefreshing()
}
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
page?.commits.count ?? 0
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "commit", for: indexPath) as! CommitCell
if let page { cell.configure(page.commits[indexPath.row], laneCount: page.laneCount) }
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
86
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let commit = page?.commits[indexPath.row] else { return }
navigationController?.pushViewController(
FilesViewController(
context: context,
owner: owner,
repository: repository,
sha: commit.sha
),
animated: true
) )
} }
private func updateEmptyView() {
tableView.backgroundView = page?.commits.isEmpty == true
? EmptyBackgroundView(title: "No commits", detail: emptyDetail)
: nil
}
} }
@MainActor @MainActor
final class RepositoryFileViewController: UIViewController, QLPreviewControllerDataSource { final class RepositoryFileViewController: UIViewController, QLPreviewControllerDataSource {
private enum MarkdownMode: Int { case preview, source }
private let context: AppContext private let context: AppContext
private let owner: String private let owner: String
private let repository: String private let repository: String
private let path: String private let path: String
private let spinner = UIActivityIndicatorView(style: .medium) private let spinner = UIActivityIndicatorView(style: .medium)
private let modeControl = UISegmentedControl()
private var loadingTask: Task<Void, Never>? private var loadingTask: Task<Void, Never>?
private var page: RepositoryFilePage?
private var previewURL: URL? private var previewURL: URL?
private var markdownSource: String?
private var fileLanguage: String? private var fileLanguage: String?
private var historyController: RepositoryHistoryViewController?
private var displayedController: UIViewController? private var displayedController: UIViewController?
private var displayedView: UIView? private var displayedView: UIView?
private weak var sourceScrollView: UIScrollView? private weak var sourceScrollView: UIScrollView?
@@ -536,13 +779,11 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
repository: repository, repository: repository,
path: path path: path
) )
guard !Task.isCancelled else { return }
self.page = page
title = page.name title = page.name
fileLanguage = page.language.isEmpty ? nil : page.language fileLanguage = page.language.isEmpty ? nil : page.language
switch page.kind { configureModes(for: page)
case "markdown": showMarkdown(page.text)
case "source": showSource(page.text)
default: try showQuickLook(page.data, name: page.name)
}
} catch { } catch {
if !Task.isCancelled { show(error: error) } if !Task.isCancelled { show(error: error) }
} }
@@ -568,26 +809,62 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
previewURL! as NSURL previewURL! as NSURL
} }
private func showMarkdown(_ source: String) { private func configureModes(for page: RepositoryFilePage) {
markdownSource = source
navigationItem.prompt = title navigationItem.prompt = title
let modeControl = UISegmentedControl(items: ["Preview", "Source"]) modeControl.removeAllSegments()
modeControl.selectedSegmentIndex = MarkdownMode.preview.rawValue let modes = page.kind == "markdown"
modeControl.addTarget(self, action: #selector(markdownModeChanged(_:)), for: .valueChanged) ? ["Preview", "Source", "History"]
modeControl.accessibilityLabel = "Markdown view" : ["Content", "History"]
for (index, mode) in modes.enumerated() {
modeControl.insertSegment(withTitle: mode, at: index, animated: false)
}
modeControl.selectedSegmentIndex = 0
modeControl.addTarget(self, action: #selector(fileModeChanged(_:)), for: .valueChanged)
modeControl.accessibilityLabel = "File view"
navigationItem.titleView = modeControl navigationItem.titleView = modeControl
showMarkdownPreview(source) showContent(page, mode: modes[0])
} }
@objc private func markdownModeChanged(_ sender: UISegmentedControl) { @objc private func fileModeChanged(_ sender: UISegmentedControl) {
guard let mode = MarkdownMode(rawValue: sender.selectedSegmentIndex), guard let page, let mode = sender.titleForSegment(at: sender.selectedSegmentIndex) else { return }
let source = markdownSource else { return } showContent(page, mode: mode)
}
private func showContent(_ page: RepositoryFilePage, mode: String) {
switch mode { switch mode {
case .preview: showMarkdownPreview(source) case "Preview": showMarkdownPreview(page.text)
case .source: showSource(source) case "Source": showSource(page.text)
case "Content" where page.kind == "source": showSource(page.text)
case "Content":
do {
try showQuickLook(page.data, name: page.name)
} catch {
show(error: error)
}
case "History": showHistory()
default: break
} }
} }
private func showHistory() {
removeDisplayedView()
let history = historyController ?? RepositoryHistoryViewController(
context: context,
owner: owner,
repository: repository,
path: path,
emptyDetail: "No commits affect this file."
)
historyController = history
addChild(history)
history.view.frame = view.bounds
history.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
displayedController = history
displayedView = history.view
view.addSubview(history.view)
history.didMove(toParent: self)
}
private func showMarkdownPreview(_ source: String) { private func showMarkdownPreview(_ source: String) {
removeDisplayedView() removeDisplayedView()
let preview = UIHostingController(rootView: RepositoryMarkdownPreview(source: source)) let preview = UIHostingController(rootView: RepositoryMarkdownPreview(source: source))
@@ -683,15 +960,20 @@ final class RepositoryFileViewController: UIViewController, QLPreviewControllerD
} }
private func showQuickLook(_ data: Data, name: String) throws { private func showQuickLook(_ data: Data, name: String) throws {
let url = FileManager.default.temporaryDirectory removeDisplayedView()
.appendingPathComponent("\(UUID().uuidString)-\(name)") if previewURL == nil {
try data.write(to: url, options: .atomic) let url = FileManager.default.temporaryDirectory
previewURL = url .appendingPathComponent("\(UUID().uuidString)-\(name)")
try data.write(to: url, options: .atomic)
previewURL = url
}
let preview = QLPreviewController() let preview = QLPreviewController()
preview.dataSource = self preview.dataSource = self
addChild(preview) addChild(preview)
preview.view.frame = view.bounds preview.view.frame = view.bounds
preview.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] preview.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
displayedController = preview
displayedView = preview.view
view.addSubview(preview.view) view.addSubview(preview.view)
preview.didMove(toParent: self) preview.didMove(toParent: self)
} }