From 1ce9bd275f38f334c079d3feda3b9624b8ed9a60 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 12 May 2026 19:51:18 +0200 Subject: [PATCH 01/22] WIP Signed-off-by: Milen Pivchev --- .gitignore | 3 +++ Brand/NCBrand.swift | 2 +- Nextcloud.xcodeproj/project.pbxproj | 4 ++++ iOSClient/Share/NCShareDetailsView.swift | 29 ++++++++++++++++++++++++ iOSClient/Share/NCSharePaging.swift | 7 ++++++ 5 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 iOSClient/Share/NCShareDetailsView.swift diff --git a/.gitignore b/.gitignore index 8e65697610..a98f72f2b3 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ Package.resolved *.generated.swift /.env-vars + +## Claude Code +.claude/ diff --git a/Brand/NCBrand.swift b/Brand/NCBrand.swift index 0fd9b5b6a7..309a1e075f 100755 --- a/Brand/NCBrand.swift +++ b/Brand/NCBrand.swift @@ -84,7 +84,7 @@ final class NCBrandOptions: @unchecked Sendable { // Info Paging enum NCInfoPagingTab: Int, CaseIterable { - case activity, sharing + case activity, sharing, details } init() { diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 8bd5feb863..cf7c1ee1b2 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -74,6 +74,7 @@ AF93471227E2341B002537EE /* NCContextMenuShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93471127E2341B002537EE /* NCContextMenuShare.swift */; }; AF93471927E2361E002537EE /* NCShareAdvancePermissionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = AF93471427E2361E002537EE /* NCShareAdvancePermissionFooter.xib */; }; AF93471A27E2361E002537EE /* NCShareHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93471527E2361E002537EE /* NCShareHeader.swift */; }; + AB7000022F70000100FE2776 /* NCShareDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7000012F70000100FE2776 /* NCShareDetailsView.swift */; }; AF93471B27E2361E002537EE /* NCShareAdvancePermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93471627E2361E002537EE /* NCShareAdvancePermission.swift */; }; AF93471C27E2361E002537EE /* NCShareHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = AF93471727E2361E002537EE /* NCShareHeader.xib */; }; AF93471D27E2361E002537EE /* NCShareAdvancePermissionFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93471827E2361E002537EE /* NCShareAdvancePermissionFooter.swift */; }; @@ -1227,6 +1228,7 @@ AF93471127E2341B002537EE /* NCContextMenuShare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuShare.swift; sourceTree = ""; }; AF93471427E2361E002537EE /* NCShareAdvancePermissionFooter.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCShareAdvancePermissionFooter.xib; sourceTree = ""; }; AF93471527E2361E002537EE /* NCShareHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareHeader.swift; sourceTree = ""; }; + AB7000012F70000100FE2776 /* NCShareDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareDetailsView.swift; sourceTree = ""; }; AF93471627E2361E002537EE /* NCShareAdvancePermission.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareAdvancePermission.swift; sourceTree = ""; }; AF93471727E2361E002537EE /* NCShareHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCShareHeader.xib; sourceTree = ""; }; AF93471827E2361E002537EE /* NCShareAdvancePermissionFooter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareAdvancePermissionFooter.swift; sourceTree = ""; }; @@ -2361,6 +2363,7 @@ AF2D7C7D2742559100ADF566 /* NCShareUserCell.swift */, AF93471727E2361E002537EE /* NCShareHeader.xib */, AF93471527E2361E002537EE /* NCShareHeader.swift */, + AB7000012F70000100FE2776 /* NCShareDetailsView.swift */, AFCE353827E5DE0400FEA6C2 /* Shareable.swift */, AA8E03D92D2ED83300E7E89C /* TransientShare.swift */, ); @@ -4478,6 +4481,7 @@ F321DA8A2B71205A00DDA0E6 /* NCTrashSelectTabBar.swift in Sources */, F702F2CD25EE5B4F008F8E80 /* AppDelegate.swift in Sources */, F769454022E9F077000A798A /* NCSharePaging.swift in Sources */, + AB7000022F70000100FE2776 /* NCShareDetailsView.swift in Sources */, F7C687E92D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */, F74D50352C9855A000BBBF4C /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift in Sources */, F7802B322BD5584F00D74270 /* NCMedia+DragDrop.swift in Sources */, diff --git a/iOSClient/Share/NCShareDetailsView.swift b/iOSClient/Share/NCShareDetailsView.swift new file mode 100644 index 0000000000..9f30645ac4 --- /dev/null +++ b/iOSClient/Share/NCShareDetailsView.swift @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI +import UIKit + +struct NCShareDetailsView: View { + var body: some View { + Text("Details") + .font(.body) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(.systemBackground)) + } +} + +final class NCShareDetailsViewController: UIHostingController, NCSharePagingContent { + var textField: UIView? { nil } + var height: CGFloat = 50 + + init() { + super.init(rootView: NCShareDetailsView()) + } + + @MainActor required dynamic init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder, rootView: NCShareDetailsView()) + } +} diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index 70f6bd7353..7c0f610d0f 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -87,6 +87,9 @@ class NCSharePaging: UIViewController { pagingViewController.borderOptions = .visible(height: 1, zIndex: Int.max, insets: .zero) + // Distribute tabs evenly across the menu width so all fit on screen. + pagingViewController.menuItemSize = .sizeToFit(minWidth: 50, height: 50) + // Contrain the paging view to all edges. pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -224,6 +227,8 @@ extension NCSharePaging: PagingViewControllerDataSource { viewController.height = height viewController.controller = controller return viewController + } else if pages[index] == .details { + return NCShareDetailsViewController() } else { return UIViewController() } @@ -235,6 +240,8 @@ extension NCSharePaging: PagingViewControllerDataSource { return PagingIndexItem(index: index, title: NSLocalizedString("_activity_", comment: "")) } else if pages[index] == .sharing { return PagingIndexItem(index: index, title: NSLocalizedString("_sharing_", comment: "")) + } else if pages[index] == .details { + return PagingIndexItem(index: index, title: NSLocalizedString("_details_", comment: "")) } else { return PagingIndexItem(index: index, title: "") } From 0b5d5ce9f54ac72733942aac2e8a53ba403cfebf Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 12 May 2026 20:02:31 +0200 Subject: [PATCH 02/22] Use segmented tabs via swiftui Signed-off-by: Milen Pivchev --- iOSClient/Share/NCSharePaging.swift | 100 +++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 8 deletions(-) diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index 7c0f610d0f..0331c0714d 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -26,6 +26,7 @@ import UIKit import Parchment import NextcloudKit import TagListView +import SwiftUI protocol NCSharePagingContent { var textField: UIView? { get } @@ -35,6 +36,9 @@ class NCSharePaging: UIViewController { private let pagingViewController = NCShareHeaderViewController() private weak var appDelegate = UIApplication.shared.delegate as? AppDelegate private var currentVC: NCSharePagingContent? + private let tabModel = NCSharePagingTabModel() + private var tabPickerHost: UIHostingController? + private var isSyncingPickerFromParchment = false var metadata = tableMetadata() var controller: NCMainTabBarController? @@ -87,9 +91,6 @@ class NCSharePaging: UIViewController { pagingViewController.borderOptions = .visible(height: 1, zIndex: Int.max, insets: .zero) - // Distribute tabs evenly across the menu width so all fit on screen. - pagingViewController.menuItemSize = .sizeToFit(minWidth: 50, height: 50) - // Contrain the paging view to all edges. pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -102,13 +103,57 @@ class NCSharePaging: UIViewController { pagingViewController.dataSource = self pagingViewController.delegate = self - if page.rawValue < pages.count { - pagingViewController.select(index: page.rawValue) - } else { - pagingViewController.select(index: 0) - } + let initialIndex = (page.rawValue < pages.count) ? page.rawValue : 0 + tabModel.selection = initialIndex + pagingViewController.select(index: initialIndex) pagingViewController.reloadMenu() + + setupTabPicker() + } + + private func setupTabPicker() { + // Hide Parchment's built-in tab strip; we drive selection from a SwiftUI segmented Picker instead. + pagingViewController.collectionView.isHidden = true + + let titles = pages.map(titleForTab(_:)) + let pickerView = NCSharePagingTabPicker( + titles: titles, + model: tabModel, + onChange: { [weak self] index in + self?.handlePickerChange(to: index) + } + ) + let host = UIHostingController(rootView: pickerView) + host.view.backgroundColor = .systemBackground + + addChild(host) + view.addSubview(host.view) + host.didMove(toParent: self) + host.view.translatesAutoresizingMaskIntoConstraints = false + + let collectionView = pagingViewController.collectionView + NSLayoutConstraint.activate([ + host.view.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor), + host.view.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor), + host.view.topAnchor.constraint(equalTo: collectionView.topAnchor), + host.view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor) + ]) + + self.tabPickerHost = host + } + + private func titleForTab(_ tab: NCBrandOptions.NCInfoPagingTab) -> String { + switch tab { + case .activity: return NSLocalizedString("_activity_", comment: "") + case .sharing: return NSLocalizedString("_sharing_", comment: "") + case .details: return NSLocalizedString("_details_", comment: "") + } + } + + private func handlePickerChange(to index: Int) { + guard !isSyncingPickerFromParchment else { return } + pagingViewController.select(index: index) } override func viewDidAppear(_ animated: Bool) { @@ -198,7 +243,19 @@ extension NCSharePaging: PagingViewControllerDelegate { func pagingViewController(_ pagingViewController: PagingViewController, willScrollToItem pagingItem: PagingItem, startingViewController: UIViewController, destinationViewController: UIViewController) { currentVC?.textField?.resignFirstResponder() + } + + func pagingViewController(_ pagingViewController: PagingViewController, didScrollToItem pagingItem: PagingItem, startingViewController: UIViewController?, destinationViewController: UIViewController, transitionSuccessful: Bool) { + + guard transitionSuccessful else { return } + self.currentVC = destinationViewController as? NCSharePagingContent + + if let item = pagingItem as? PagingIndexItem { + isSyncingPickerFromParchment = true + tabModel.selection = item.index + isSyncingPickerFromParchment = false + } } } @@ -322,3 +379,30 @@ class NCSharePagingView: PagingView { ]) } } + +// MARK: - SwiftUI segmented tab picker + +@Observable +final class NCSharePagingTabModel { + var selection: Int = 0 +} + +struct NCSharePagingTabPicker: View { + let titles: [String] + @Bindable var model: NCSharePagingTabModel + var onChange: (Int) -> Void + + var body: some View { + Picker("", selection: $model.selection) { + ForEach(Array(titles.enumerated()), id: \.offset) { index, title in + Text(title).tag(index) + } + } + .pickerStyle(.segmented) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .onChange(of: model.selection) { _, newValue in + onChange(newValue) + } + } +} From 1dc1adc0df4fc346e2e5e42a3de83a9e18135aac Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 12 May 2026 20:10:15 +0200 Subject: [PATCH 03/22] Remove Parchment Signed-off-by: Milen Pivchev --- iOSClient/Share/NCSharePaging.swift | 344 +++++++++------------------- 1 file changed, 111 insertions(+), 233 deletions(-) diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index 0331c0714d..4dd0ac6a5b 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -23,7 +23,6 @@ // import UIKit -import Parchment import NextcloudKit import TagListView import SwiftUI @@ -33,17 +32,36 @@ protocol NCSharePagingContent { } class NCSharePaging: UIViewController { - private let pagingViewController = NCShareHeaderViewController() private weak var appDelegate = UIApplication.shared.delegate as? AppDelegate - private var currentVC: NCSharePagingContent? private let tabModel = NCSharePagingTabModel() - private var tabPickerHost: UIHostingController? - private var isSyncingPickerFromParchment = false + private weak var headerView: NCShareHeader? + private var pageVCs: [UIViewController] = [] + private var contentHost: UIHostingController? var metadata = tableMetadata() var controller: NCMainTabBarController? var pages: [NCBrandOptions.NCInfoPagingTab] = [] - var page: NCBrandOptions.NCInfoPagingTab = .activity + + private var initialPage: NCBrandOptions.NCInfoPagingTab = .activity + var page: NCBrandOptions.NCInfoPagingTab { + get { + guard isViewLoaded else { return initialPage } + let index = tabModel.selection + return (index < pages.count) ? pages[index] : initialPage + } + set { + initialPage = newValue + if isViewLoaded, let index = pages.firstIndex(of: newValue) { + tabModel.selection = index + } + } + } + + private var currentVC: NCSharePagingContent? { + let index = tabModel.selection + guard index < pageVCs.count else { return nil } + return pageVCs[index] as? NCSharePagingContent + } // MARK: - View Life Cycle @@ -67,64 +85,37 @@ class NCSharePaging: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(notification:)), name: UIApplication.didEnterBackgroundNotification, object: nil) - // *** MUST BE THE FIRST ONE *** - pagingViewController.metadata = metadata - pagingViewController.backgroundColor = .systemBackground - pagingViewController.menuBackgroundColor = .systemBackground - pagingViewController.selectedBackgroundColor = .systemBackground - pagingViewController.indicatorColor = NCBrandColor.shared.getElement(account: metadata.account) - pagingViewController.textColor = NCBrandColor.shared.textColor - pagingViewController.selectedTextColor = NCBrandColor.shared.getElement(account: metadata.account) - - // Pagination - addChild(pagingViewController) - view.addSubview(pagingViewController.view) - pagingViewController.didMove(toParent: self) - - // Customization - pagingViewController.indicatorOptions = .visible( - height: 1, - zIndex: Int.max, - spacing: .zero, - insets: .zero - ) - - pagingViewController.borderOptions = .visible(height: 1, zIndex: Int.max, insets: .zero) - - // Contrain the paging view to all edges. - pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - pagingViewController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - pagingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), - pagingViewController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), - pagingViewController.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) - ]) + setupHeader() - pagingViewController.dataSource = self - pagingViewController.delegate = self + pageVCs = pages.map { makeViewController(for: $0) } + tabModel.selection = pages.firstIndex(of: initialPage) ?? 0 - let initialIndex = (page.rawValue < pages.count) ? page.rawValue : 0 - tabModel.selection = initialIndex - pagingViewController.select(index: initialIndex) + setupContent() + } - pagingViewController.reloadMenu() + private func setupHeader() { + guard let headerView = Bundle.main.loadNibNamed("NCShareHeader", owner: self, options: nil)?.first as? NCShareHeader else { return } + self.headerView = headerView + headerView.backgroundColor = .systemBackground + headerView.setupUI(with: metadata) - setupTabPicker() + view.addSubview(headerView) + headerView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + headerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + headerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + headerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) + ]) } - private func setupTabPicker() { - // Hide Parchment's built-in tab strip; we drive selection from a SwiftUI segmented Picker instead. - pagingViewController.collectionView.isHidden = true - - let titles = pages.map(titleForTab(_:)) - let pickerView = NCSharePagingTabPicker( - titles: titles, + private func setupContent() { + let content = NCSharePagingContentView( model: tabModel, - onChange: { [weak self] index in - self?.handlePickerChange(to: index) - } + tint: Color(NCBrandColor.shared.getElement(account: metadata.account)), + titles: pages.map(titleForTab(_:)), + pageVCs: pageVCs ) - let host = UIHostingController(rootView: pickerView) + let host = UIHostingController(rootView: content) host.view.backgroundColor = .systemBackground addChild(host) @@ -132,15 +123,43 @@ class NCSharePaging: UIViewController { host.didMove(toParent: self) host.view.translatesAutoresizingMaskIntoConstraints = false - let collectionView = pagingViewController.collectionView + let topAnchor = headerView?.bottomAnchor ?? view.safeAreaLayoutGuide.topAnchor NSLayoutConstraint.activate([ - host.view.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor), - host.view.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor), - host.view.topAnchor.constraint(equalTo: collectionView.topAnchor), - host.view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor) + host.view.topAnchor.constraint(equalTo: topAnchor), + host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + host.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + host.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor) ]) - self.tabPickerHost = host + self.contentHost = host + } + + private func makeViewController(for tab: NCBrandOptions.NCInfoPagingTab) -> UIViewController { + let height: CGFloat = 50 + + switch tab { + case .activity: + guard let viewController = UIStoryboard(name: "NCActivity", bundle: nil).instantiateInitialViewController() as? NCActivity else { + return UIViewController() + } + viewController.height = height + viewController.showComments = true + viewController.didSelectItemEnable = false + viewController.metadata = metadata + viewController.objectType = "files" + viewController.account = metadata.account + return viewController + case .sharing: + guard let viewController = UIStoryboard(name: "NCShare", bundle: nil).instantiateViewController(withIdentifier: "sharing") as? NCShare else { + return UIViewController() + } + viewController.metadata = metadata + viewController.height = height + viewController.controller = controller + return viewController + case .details: + return NCShareDetailsViewController() + } } private func titleForTab(_ tab: NCBrandOptions.NCInfoPagingTab) -> String { @@ -151,16 +170,6 @@ class NCSharePaging: UIViewController { } } - private func handlePickerChange(to index: Int) { - guard !isSyncingPickerFromParchment else { return } - pagingViewController.select(index: index) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - currentVC = pagingViewController.pageViewController.selectedViewController as? NCSharePagingContent - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -219,16 +228,12 @@ class NCSharePaging: UIViewController { } @objc func editTagsTapped(_ sender: Any?) { - guard let header = (pagingViewController.view as? NCSharePagingView)?.header else { - return - } + guard let header = headerView else { return } header.presentTagEditor(from: self) { [weak self] tags in guard let self else { return } self.metadata.tags.removeAll() self.metadata.tags.append(objectsIn: tags, account: self.metadata.account) - self.pagingViewController.metadata.tags.removeAll() - self.pagingViewController.metadata.tags.append(objectsIn: tags, account: self.pagingViewController.metadata.account) } } @@ -237,172 +242,45 @@ class NCSharePaging: UIViewController { } } -// MARK: - PagingViewController Delegate - -extension NCSharePaging: PagingViewControllerDelegate { - func pagingViewController(_ pagingViewController: PagingViewController, willScrollToItem pagingItem: PagingItem, startingViewController: UIViewController, destinationViewController: UIViewController) { - - currentVC?.textField?.resignFirstResponder() - } - - func pagingViewController(_ pagingViewController: PagingViewController, didScrollToItem pagingItem: PagingItem, startingViewController: UIViewController?, destinationViewController: UIViewController, transitionSuccessful: Bool) { - - guard transitionSuccessful else { return } - - self.currentVC = destinationViewController as? NCSharePagingContent - - if let item = pagingItem as? PagingIndexItem { - isSyncingPickerFromParchment = true - tabModel.selection = item.index - isSyncingPickerFromParchment = false - } - } -} - -// MARK: - PagingViewController DataSource - -extension NCSharePaging: PagingViewControllerDataSource { - func pagingViewController(_: PagingViewController, viewControllerAt index: Int) -> UIViewController { - let height: CGFloat = 50 - - if pages[index] == .activity { - guard let viewController = UIStoryboard(name: "NCActivity", bundle: nil).instantiateInitialViewController() as? NCActivity else { - return UIViewController() - } - viewController.height = height - viewController.showComments = true - viewController.didSelectItemEnable = false - viewController.metadata = metadata - viewController.objectType = "files" - viewController.account = metadata.account - return viewController - } else if pages[index] == .sharing { - guard let viewController = UIStoryboard(name: "NCShare", bundle: nil).instantiateViewController(withIdentifier: "sharing") as? NCShare else { - return UIViewController() - } - viewController.metadata = metadata - viewController.height = height - viewController.controller = controller - return viewController - } else if pages[index] == .details { - return NCShareDetailsViewController() - } else { - return UIViewController() - } - } - - func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem { - - if pages[index] == .activity { - return PagingIndexItem(index: index, title: NSLocalizedString("_activity_", comment: "")) - } else if pages[index] == .sharing { - return PagingIndexItem(index: index, title: NSLocalizedString("_sharing_", comment: "")) - } else if pages[index] == .details { - return PagingIndexItem(index: index, title: NSLocalizedString("_details_", comment: "")) - } else { - return PagingIndexItem(index: index, title: "") - } - } - - func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int { - return self.pages.count - } -} - -// MARK: - Header - -class NCShareHeaderViewController: PagingViewController { - public var image: UIImage? - public var metadata = tableMetadata() - - override func loadView() { - view = NCSharePagingView( - options: options, - collectionView: collectionView, - pageView: pageViewController.view, - metadata: metadata - ) - } -} - -class NCSharePagingView: PagingView { - var metadata = tableMetadata() - let utilityFileSystem = NCUtilityFileSystem() - let utility = NCUtility() - public var headerHeightConstraint: NSLayoutConstraint? - var header: NCShareHeader? - - // MARK: - View Life Cycle - - public init(options: Parchment.PagingOptions, collectionView: UICollectionView, pageView: UIView, metadata: tableMetadata) { - super.init(options: options, collectionView: collectionView, pageView: pageView) - - self.metadata = metadata - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func setupConstraints() { - guard let headerView = Bundle.main.loadNibNamed("NCShareHeader", owner: self, options: nil)?.first as? NCShareHeader else { return } - header = headerView - headerView.backgroundColor = .systemBackground - - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .short - dateFormatter.timeStyle = .short - dateFormatter.locale = Locale.current - - headerView.setupUI(with: metadata) - - addSubview(headerView) - - collectionView.translatesAutoresizingMaskIntoConstraints = false - headerView.translatesAutoresizingMaskIntoConstraints = false - pageView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: trailingAnchor), - collectionView.heightAnchor.constraint(equalToConstant: options.menuHeight), - collectionView.topAnchor.constraint(equalTo: headerView.bottomAnchor), - - headerView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor), - headerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor), - headerView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor), - - pageView.leadingAnchor.constraint(equalTo: leadingAnchor), - pageView.trailingAnchor.constraint(equalTo: trailingAnchor), - pageView.bottomAnchor.constraint(equalTo: bottomAnchor), - pageView.topAnchor.constraint(equalTo: headerView.bottomAnchor) - ]) - } -} - -// MARK: - SwiftUI segmented tab picker +// MARK: - SwiftUI tab content @Observable final class NCSharePagingTabModel { var selection: Int = 0 } -struct NCSharePagingTabPicker: View { - let titles: [String] +struct NCSharePagingContentView: View { @Bindable var model: NCSharePagingTabModel - var onChange: (Int) -> Void + let tint: Color + let titles: [String] + let pageVCs: [UIViewController] var body: some View { - Picker("", selection: $model.selection) { - ForEach(Array(titles.enumerated()), id: \.offset) { index, title in - Text(title).tag(index) + VStack(spacing: 0) { + Picker("", selection: $model.selection) { + ForEach(Array(titles.enumerated()), id: \.offset) { index, title in + Text(title).tag(index) + } } - } - .pickerStyle(.segmented) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .onChange(of: model.selection) { _, newValue in - onChange(newValue) + .pickerStyle(.segmented) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .tint(tint) + + TabView(selection: $model.selection) { + ForEach(Array(pageVCs.enumerated()), id: \.offset) { index, viewController in + NCViewControllerRepresentable(viewController: viewController) + .tag(index) + } + } + .tabViewStyle(.page(indexDisplayMode: .never)) } } } + +private struct NCViewControllerRepresentable: UIViewControllerRepresentable { + let viewController: UIViewController + + func makeUIViewController(context: Context) -> UIViewController { viewController } + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} From 6fdf18fd4852f53fb170a12ee10be3c5f1ee05a2 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Fri, 15 May 2026 16:39:25 +0200 Subject: [PATCH 04/22] Remove Parchment Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 29 ++--------------------------- iOSClient/Share/NCShare.swift | 1 - iOSClient/Share/NCSharePaging.swift | 13 +++++++++++-- 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index cf7c1ee1b2..153096839e 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ AAE330042D2ED20200B04903 /* NCShareNavigationTitleSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE330032D2ED1FF00B04903 /* NCShareNavigationTitleSetting.swift */; }; AB6000012F60000100FE2775 /* NCTagEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000002F60000100FE2775 /* NCTagEditorModel.swift */; }; AB6000032F60000200FE2775 /* NCTagEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000022F60000200FE2775 /* NCTagEditorView.swift */; }; + AB7000022F70000100FE2776 /* NCShareDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7000012F70000100FE2776 /* NCShareDetailsView.swift */; }; AF1A9B6427D0CA1E00F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */ = {isa = PBXBuildFile; fileRef = F704B5E42430AA8000632F5F /* NCCreateFormUploadConflict.swift */; }; @@ -74,7 +75,6 @@ AF93471227E2341B002537EE /* NCContextMenuShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93471127E2341B002537EE /* NCContextMenuShare.swift */; }; AF93471927E2361E002537EE /* NCShareAdvancePermissionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = AF93471427E2361E002537EE /* NCShareAdvancePermissionFooter.xib */; }; AF93471A27E2361E002537EE /* NCShareHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93471527E2361E002537EE /* NCShareHeader.swift */; }; - AB7000022F70000100FE2776 /* NCShareDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7000012F70000100FE2776 /* NCShareDetailsView.swift */; }; AF93471B27E2361E002537EE /* NCShareAdvancePermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93471627E2361E002537EE /* NCShareAdvancePermission.swift */; }; AF93471C27E2361E002537EE /* NCShareHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = AF93471727E2361E002537EE /* NCShareHeader.xib */; }; AF93471D27E2361E002537EE /* NCShareAdvancePermissionFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF93471827E2361E002537EE /* NCShareAdvancePermissionFooter.swift */; }; @@ -133,7 +133,6 @@ F37208AA2BAB63EE006B5430 /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = F37208A92BAB63EE006B5430 /* MarkdownKit */; }; F37208B02BAB63EF006B5430 /* Queuer in Frameworks */ = {isa = PBXBuildFile; productRef = F37208AF2BAB63EF006B5430 /* Queuer */; }; F37208B22BAB63EF006B5430 /* DropDown in Frameworks */ = {isa = PBXBuildFile; productRef = F37208B12BAB63EF006B5430 /* DropDown */; }; - F37208B42BAB63EF006B5430 /* Parchment in Frameworks */ = {isa = PBXBuildFile; productRef = F37208B32BAB63EF006B5430 /* Parchment */; }; F37208B62BAB63EF006B5430 /* EasyTipView in Frameworks */ = {isa = PBXBuildFile; productRef = F37208B52BAB63EF006B5430 /* EasyTipView */; }; F37208B82BAB63EF006B5430 /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = F37208B72BAB63EF006B5430 /* NextcloudKit */; }; F37208BA2BAB63EF006B5430 /* TLPhotoPicker in Frameworks */ = {isa = PBXBuildFile; productRef = F37208B92BAB63EF006B5430 /* TLPhotoPicker */; }; @@ -720,7 +719,6 @@ F7B934FE2BDCFE1E002B2FC9 /* NCDragDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B934FD2BDCFE1E002B2FC9 /* NCDragDrop.swift */; }; F7BAADCB1ED5A87C00B7EAD4 /* NCManageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */; }; F7BAADCC1ED5A87C00B7EAD4 /* NCManageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BAADB51ED5A87C00B7EAD4 /* NCManageDatabase.swift */; }; - F7BB7E4727A18C56009B9F29 /* Parchment in Frameworks */ = {isa = PBXBuildFile; productRef = F7BB7E4627A18C56009B9F29 /* Parchment */; }; F7BC287E26663F6C004D46C5 /* NCViewCertificateDetails.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7BC287D26663F6C004D46C5 /* NCViewCertificateDetails.storyboard */; }; F7BC288026663F85004D46C5 /* NCViewCertificateDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BC287F26663F85004D46C5 /* NCViewCertificateDetails.swift */; }; F7BD0A002C468925003A4A6D /* NCMedia+CollectionViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7BD09FF2C468925003A4A6D /* NCMedia+CollectionViewDataSource.swift */; }; @@ -1210,6 +1208,7 @@ AAE330032D2ED1FF00B04903 /* NCShareNavigationTitleSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareNavigationTitleSetting.swift; sourceTree = ""; }; AB6000002F60000100FE2775 /* NCTagEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorModel.swift; sourceTree = ""; }; AB6000022F60000200FE2775 /* NCTagEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorView.swift; sourceTree = ""; }; + AB7000012F70000100FE2776 /* NCShareDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareDetailsView.swift; sourceTree = ""; }; AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extension.swift"; sourceTree = ""; }; AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareCell.swift; sourceTree = ""; }; AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+DataSource.swift"; sourceTree = ""; }; @@ -1228,7 +1227,6 @@ AF93471127E2341B002537EE /* NCContextMenuShare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuShare.swift; sourceTree = ""; }; AF93471427E2361E002537EE /* NCShareAdvancePermissionFooter.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCShareAdvancePermissionFooter.xib; sourceTree = ""; }; AF93471527E2361E002537EE /* NCShareHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareHeader.swift; sourceTree = ""; }; - AB7000012F70000100FE2776 /* NCShareDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareDetailsView.swift; sourceTree = ""; }; AF93471627E2361E002537EE /* NCShareAdvancePermission.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareAdvancePermission.swift; sourceTree = ""; }; AF93471727E2361E002537EE /* NCShareHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCShareHeader.xib; sourceTree = ""; }; AF93471827E2361E002537EE /* NCShareAdvancePermissionFooter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareAdvancePermissionFooter.swift; sourceTree = ""; }; @@ -1878,7 +1876,6 @@ F37208B22BAB63EF006B5430 /* DropDown in Frameworks */, F37208BC2BAB63EF006B5430 /* OpenSSL in Frameworks */, F37208B82BAB63EF006B5430 /* NextcloudKit in Frameworks */, - F37208B42BAB63EF006B5430 /* Parchment in Frameworks */, F37208BE2BAB63F0006B5430 /* Swifter in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1954,7 +1951,6 @@ F788ECC7263AAAFA00ADC67F /* MarkdownKit in Frameworks */, F3374AF02D78B01B002A38F9 /* BitCollections in Frameworks */, F77BC3EB293E5268005F2B08 /* Swifter in Frameworks */, - F7BB7E4727A18C56009B9F29 /* Parchment in Frameworks */, F33EE6E12BF4BDA500CA1A51 /* NIOSSL in Frameworks */, F734B06628E75C0100E180D5 /* TLPhotoPicker in Frameworks */, F760DE032AE66EA80027D78A /* KeychainAccess in Frameworks */, @@ -3453,7 +3449,6 @@ F37208A92BAB63EE006B5430 /* MarkdownKit */, F37208AF2BAB63EF006B5430 /* Queuer */, F37208B12BAB63EF006B5430 /* DropDown */, - F37208B32BAB63EF006B5430 /* Parchment */, F37208B52BAB63EF006B5430 /* EasyTipView */, F37208B72BAB63EF006B5430 /* NextcloudKit */, F37208B92BAB63EF006B5430 /* TLPhotoPicker */, @@ -3626,7 +3621,6 @@ F70B86742642CE3B00ED5349 /* FirebaseCrashlytics */, F76DA962277B760E0082465B /* Queuer */, F76DA968277B77EA0082465B /* DropDown */, - F7BB7E4627A18C56009B9F29 /* Parchment */, F753BA92281FD8020015BFB6 /* EasyTipView */, F72AD70C28C24B93006CB92D /* NextcloudKit */, F734B06528E75C0100E180D5 /* TLPhotoPicker */, @@ -3809,7 +3803,6 @@ F76DA961277B760E0082465B /* XCRemoteSwiftPackageReference "Queuer" */, F76DA967277B77E90082465B /* XCRemoteSwiftPackageReference "DropDown" */, F710FC78277B7CFF00AA9FBF /* XCRemoteSwiftPackageReference "realm-swift" */, - F7BB7E4527A18C56009B9F29 /* XCRemoteSwiftPackageReference "Parchment" */, F753BA91281FD8010015BFB6 /* XCRemoteSwiftPackageReference "EasyTipView" */, F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */, F734B06428E75C0100E180D5 /* XCRemoteSwiftPackageReference "TLPhotoPicker" */, @@ -6156,14 +6149,6 @@ minimumVersion = 1.0.0; }; }; - F7BB7E4527A18C56009B9F29 /* XCRemoteSwiftPackageReference "Parchment" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/rechsteiner/Parchment"; - requirement = { - kind = exactVersion; - version = 3.3.0; - }; - }; F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/tylerjonesio/vlckit-spm"; @@ -6289,11 +6274,6 @@ package = F76DA967277B77E90082465B /* XCRemoteSwiftPackageReference "DropDown" */; productName = DropDown; }; - F37208B32BAB63EF006B5430 /* Parchment */ = { - isa = XCSwiftPackageProductDependency; - package = F7BB7E4527A18C56009B9F29 /* XCRemoteSwiftPackageReference "Parchment" */; - productName = Parchment; - }; F37208B52BAB63EF006B5430 /* EasyTipView */ = { isa = XCSwiftPackageProductDependency; package = F753BA91281FD8010015BFB6 /* XCRemoteSwiftPackageReference "EasyTipView" */; @@ -6574,11 +6554,6 @@ package = F710FC78277B7CFF00AA9FBF /* XCRemoteSwiftPackageReference "realm-swift" */; productName = RealmSwift; }; - F7BB7E4627A18C56009B9F29 /* Parchment */ = { - isa = XCSwiftPackageProductDependency; - package = F7BB7E4527A18C56009B9F29 /* XCRemoteSwiftPackageReference "Parchment" */; - productName = Parchment; - }; F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */ = { isa = XCSwiftPackageProductDependency; package = F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */; diff --git a/iOSClient/Share/NCShare.swift b/iOSClient/Share/NCShare.swift index 2f2a765db8..0600a2ecc9 100644 --- a/iOSClient/Share/NCShare.swift +++ b/iOSClient/Share/NCShare.swift @@ -24,7 +24,6 @@ // import UIKit -import Parchment import DropDown import NextcloudKit import ContactsUI diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index 4dd0ac6a5b..5774c7877c 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -113,7 +113,10 @@ class NCSharePaging: UIViewController { model: tabModel, tint: Color(NCBrandColor.shared.getElement(account: metadata.account)), titles: pages.map(titleForTab(_:)), - pageVCs: pageVCs + pageVCs: pageVCs, + onSelectionChange: { [weak self] _ in + self?.view.endEditing(true) + } ) let host = UIHostingController(rootView: content) host.view.backgroundColor = .systemBackground @@ -135,7 +138,9 @@ class NCSharePaging: UIViewController { } private func makeViewController(for tab: NCBrandOptions.NCInfoPagingTab) -> UIViewController { - let height: CGFloat = 50 + // The old Parchment menu floated over the child view, so children inset by menuHeight (50). + // The new SwiftUI layout places the picker above the content, so no inset is needed. + let height: CGFloat = 0 switch tab { case .activity: @@ -254,6 +259,7 @@ struct NCSharePagingContentView: View { let tint: Color let titles: [String] let pageVCs: [UIViewController] + var onSelectionChange: (Int) -> Void var body: some View { VStack(spacing: 0) { @@ -275,6 +281,9 @@ struct NCSharePagingContentView: View { } .tabViewStyle(.page(indexDisplayMode: .never)) } + .onChange(of: model.selection) { _, newValue in + onSelectionChange(newValue) + } } } From 3d240ae0adfb9b564c67c8a68b29a3026729aa9a Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 18 May 2026 12:28:57 +0200 Subject: [PATCH 05/22] Governance UI Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 4 + iOSClient/Activity/NCActivity.storyboard | 1 + iOSClient/Activity/NCActivity.swift | 27 ++++--- iOSClient/Activity/NCActivityCommentView.xib | 4 +- iOSClient/Share/NCShare.swift | 4 +- .../Share/NCShareDetailsGovernanceModel.swift | 81 +++++++++++++++++++ iOSClient/Share/NCShareDetailsView.swift | 55 ++++++++++--- iOSClient/Share/NCSharePaging.swift | 9 ++- .../en.lproj/Localizable.strings | 8 ++ 9 files changed, 163 insertions(+), 30 deletions(-) create mode 100644 iOSClient/Share/NCShareDetailsGovernanceModel.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 153096839e..1ada057813 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ AB6000012F60000100FE2775 /* NCTagEditorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000002F60000100FE2775 /* NCTagEditorModel.swift */; }; AB6000032F60000200FE2775 /* NCTagEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000022F60000200FE2775 /* NCTagEditorView.swift */; }; AB7000022F70000100FE2776 /* NCShareDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7000012F70000100FE2776 /* NCShareDetailsView.swift */; }; + AB7000042F70000100FE2776 /* NCShareDetailsGovernanceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7000032F70000100FE2776 /* NCShareDetailsGovernanceModel.swift */; }; AF1A9B6427D0CA1E00F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */ = {isa = PBXBuildFile; fileRef = F704B5E42430AA8000632F5F /* NCCreateFormUploadConflict.swift */; }; @@ -1209,6 +1210,7 @@ AB6000002F60000100FE2775 /* NCTagEditorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorModel.swift; sourceTree = ""; }; AB6000022F60000200FE2775 /* NCTagEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorView.swift; sourceTree = ""; }; AB7000012F70000100FE2776 /* NCShareDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareDetailsView.swift; sourceTree = ""; }; + AB7000032F70000100FE2776 /* NCShareDetailsGovernanceModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareDetailsGovernanceModel.swift; sourceTree = ""; }; AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extension.swift"; sourceTree = ""; }; AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareCell.swift; sourceTree = ""; }; AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+DataSource.swift"; sourceTree = ""; }; @@ -2360,6 +2362,7 @@ AF93471727E2361E002537EE /* NCShareHeader.xib */, AF93471527E2361E002537EE /* NCShareHeader.swift */, AB7000012F70000100FE2776 /* NCShareDetailsView.swift */, + AB7000032F70000100FE2776 /* NCShareDetailsGovernanceModel.swift */, AFCE353827E5DE0400FEA6C2 /* Shareable.swift */, AA8E03D92D2ED83300E7E89C /* TransientShare.swift */, ); @@ -4475,6 +4478,7 @@ F702F2CD25EE5B4F008F8E80 /* AppDelegate.swift in Sources */, F769454022E9F077000A798A /* NCSharePaging.swift in Sources */, AB7000022F70000100FE2776 /* NCShareDetailsView.swift in Sources */, + AB7000042F70000100FE2776 /* NCShareDetailsGovernanceModel.swift in Sources */, F7C687E92D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */, F74D50352C9855A000BBBF4C /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift in Sources */, F7802B322BD5584F00D74270 /* NCMedia+DragDrop.swift in Sources */, diff --git a/iOSClient/Activity/NCActivity.storyboard b/iOSClient/Activity/NCActivity.storyboard index e156b6070b..8d5210d921 100644 --- a/iOSClient/Activity/NCActivity.storyboard +++ b/iOSClient/Activity/NCActivity.storyboard @@ -59,6 +59,7 @@ + diff --git a/iOSClient/Activity/NCActivity.swift b/iOSClient/Activity/NCActivity.swift index 10df054eb5..efcb6117f9 100644 --- a/iOSClient/Activity/NCActivity.swift +++ b/iOSClient/Activity/NCActivity.swift @@ -15,6 +15,7 @@ class NCActivity: UIViewController, NCSharePagingContent { var height: CGFloat = 0 var metadata: tableMetadata? var showComments: Bool = false + var usesGroupedBackground: Bool = false let utilityFileSystem = NCUtilityFileSystem() let utility = NCUtility() @@ -55,13 +56,13 @@ class NCActivity: UIViewController, NCSharePagingContent { super.viewDidLoad() navigationController?.setNavigationBarAppearance() - view.backgroundColor = .systemBackground + view.backgroundColor = usesGroupedBackground ? .systemGroupedBackground : .systemBackground self.title = NSLocalizedString("_activity_", comment: "") tableView.allowsSelection = false tableView.separatorColor = UIColor.clear tableView.contentInset = insets - tableView.backgroundColor = .systemBackground + tableView.backgroundColor = usesGroupedBackground ? .systemGroupedBackground : .systemBackground if showComments { setupComments() @@ -164,25 +165,25 @@ extension NCActivity: UITableViewDelegate { label.text = utility.getTitleFromDate(sectionDates[section]) label.textAlignment = .center - let blur = UIBlurEffect(style: .systemMaterial) - let blurredEffectView = UIVisualEffectView(effect: blur) - blurredEffectView.layer.cornerRadius = 11 - blurredEffectView.layer.masksToBounds = true + let pill = UIView() + pill.backgroundColor = .systemGray5 + pill.layer.cornerRadius = 11 + pill.layer.masksToBounds = true - view.addSubview(blurredEffectView) + view.addSubview(pill) view.addSubview(label) - blurredEffectView.translatesAutoresizingMaskIntoConstraints = false + pill.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - blurredEffectView.topAnchor.constraint(equalTo: view.topAnchor), - blurredEffectView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - blurredEffectView.widthAnchor.constraint(equalToConstant: label.intrinsicContentSize.width + 30), - blurredEffectView.heightAnchor.constraint(equalToConstant: 22), + pill.topAnchor.constraint(equalTo: view.topAnchor), + pill.centerXAnchor.constraint(equalTo: view.centerXAnchor), + pill.widthAnchor.constraint(equalToConstant: label.intrinsicContentSize.width + 30), + pill.heightAnchor.constraint(equalToConstant: 22), label.topAnchor.constraint(equalTo: view.topAnchor), label.centerXAnchor.constraint(equalTo: view.centerXAnchor), - label.centerYAnchor.constraint(equalTo: blurredEffectView.centerYAnchor) + label.centerYAnchor.constraint(equalTo: pill.centerYAnchor) ]) return view diff --git a/iOSClient/Activity/NCActivityCommentView.xib b/iOSClient/Activity/NCActivityCommentView.xib index e60f0514d4..d5113e2f58 100644 --- a/iOSClient/Activity/NCActivityCommentView.xib +++ b/iOSClient/Activity/NCActivityCommentView.xib @@ -23,7 +23,7 @@ - + @@ -32,7 +32,7 @@ - + diff --git a/iOSClient/Share/NCShare.swift b/iOSClient/Share/NCShare.swift index 0600a2ecc9..59415f2f63 100644 --- a/iOSClient/Share/NCShare.swift +++ b/iOSClient/Share/NCShare.swift @@ -74,7 +74,7 @@ class NCShare: UIViewController, NCSharePagingContent { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .systemBackground + view.backgroundColor = .systemGroupedBackground viewContainerConstraint.constant = height searchFieldTopConstraint.constant = 0 @@ -85,7 +85,7 @@ class NCShare: UIViewController, NCSharePagingContent { tableView.dataSource = self tableView.delegate = self tableView.allowsSelection = false - tableView.backgroundColor = .systemBackground + tableView.backgroundColor = .systemGroupedBackground tableView.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 10, right: 0) tableView.register(UINib(nibName: "NCShareLinkCell", bundle: nil), forCellReuseIdentifier: "cellLink") diff --git a/iOSClient/Share/NCShareDetailsGovernanceModel.swift b/iOSClient/Share/NCShareDetailsGovernanceModel.swift new file mode 100644 index 0000000000..8b98a5a9d9 --- /dev/null +++ b/iOSClient/Share/NCShareDetailsGovernanceModel.swift @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import Foundation +import NextcloudKit + +@MainActor +@Observable +final class NCShareDetailsGovernanceModel { + enum SensitivityLabel: String, CaseIterable, Identifiable { + case publicLabel, internalUseOnly, restricted + + var id: String { rawValue } + + var localizedName: String { + switch self { + case .publicLabel: return NSLocalizedString("_public_", comment: "") + case .internalUseOnly: return NSLocalizedString("_internal_use_only_", comment: "") + case .restricted: return NSLocalizedString("_restricted_", comment: "") + } + } + + var systemImageName: String { + switch self { + case .publicLabel: return "link" + case .internalUseOnly: return "person.2" + case .restricted: return "xmark" + } + } + } + + enum RetentionPolicy: String, CaseIterable, Identifiable { + case publicPolicy, internalUseOnly, restricted + + var id: String { rawValue } + + var localizedName: String { + switch self { + case .publicPolicy: return NSLocalizedString("_public_", comment: "") + case .internalUseOnly: return NSLocalizedString("_internal_use_only_", comment: "") + case .restricted: return NSLocalizedString("_restricted_", comment: "") + } + } + + var systemImageName: String { + switch self { + case .publicPolicy: return "link" + case .internalUseOnly: return "person.2" + case .restricted: return "xmark" + } + } + } + + var selectedSensitivityLabel: SensitivityLabel = .publicLabel + var selectedRetentionPolicy: RetentionPolicy = .publicPolicy + + private let metadata: tableMetadata + + init(metadata: tableMetadata) { + self.metadata = metadata + } + + var account: String { metadata.account } + + func getSensitivityLabels() -> [SensitivityLabel] { + SensitivityLabel.allCases + } + + func getRetentionPolicies() -> [RetentionPolicy] { + RetentionPolicy.allCases + } + + func setSensitivityLabel(_ label: SensitivityLabel) { + selectedSensitivityLabel = label + } + + func setRetentionPolicy(_ policy: RetentionPolicy) { + selectedRetentionPolicy = policy + } +} diff --git a/iOSClient/Share/NCShareDetailsView.swift b/iOSClient/Share/NCShareDetailsView.swift index 9f30645ac4..9f3a8f365c 100644 --- a/iOSClient/Share/NCShareDetailsView.swift +++ b/iOSClient/Share/NCShareDetailsView.swift @@ -6,24 +6,61 @@ import SwiftUI import UIKit struct NCShareDetailsView: View { + @State private var model: NCShareDetailsGovernanceModel + + init(metadata: tableMetadata) { + _model = State(initialValue: NCShareDetailsGovernanceModel(metadata: metadata)) + } + var body: some View { - Text("Details") - .font(.body) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(.systemBackground)) + Form { + Section(header: + Text(NSLocalizedString("_governance_", comment: "")).font(.headline) + ) { + HStack { + Text(NSLocalizedString("_sensitivity_label_", comment: "")) + .cappedFont(.body, maxDynamicType: .accessibility2) + Spacer() + Picker("", selection: $model.selectedSensitivityLabel) { + ForEach(model.getSensitivityLabels()) { label in + Label(label.localizedName, systemImage: label.systemImageName).tag(label) + } + } + .pickerStyle(.menu) + } + .onChange(of: model.selectedSensitivityLabel) { _, newValue in + model.setSensitivityLabel(newValue) + } + + HStack { + Text(NSLocalizedString("_file_retention_", comment: "")) + .cappedFont(.body, maxDynamicType: .accessibility2) + Spacer() + Picker("", selection: $model.selectedRetentionPolicy) { + ForEach(model.getRetentionPolicies()) { policy in + Label(policy.localizedName, systemImage: policy.systemImageName).tag(policy) + } + } + .pickerStyle(.menu) + } + .onChange(of: model.selectedRetentionPolicy) { _, newValue in + model.setRetentionPolicy(newValue) + } + } + } + .tint(Color(NCBrandColor.shared.getElement(account: model.account))) } } final class NCShareDetailsViewController: UIHostingController, NCSharePagingContent { var textField: UIView? { nil } - var height: CGFloat = 50 + var height: CGFloat = 0 - init() { - super.init(rootView: NCShareDetailsView()) + init(metadata: tableMetadata) { + super.init(rootView: NCShareDetailsView(metadata: metadata)) } @MainActor required dynamic init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder, rootView: NCShareDetailsView()) + fatalError("init(coder:) not supported") } } diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index 5774c7877c..51abf5790c 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -68,7 +68,7 @@ class NCSharePaging: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .systemBackground + view.backgroundColor = .systemGroupedBackground title = NSLocalizedString("_details_", comment: "") navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("_close_", comment: ""), style: .plain, target: self, action: #selector(exitTapped(_:))) @@ -96,7 +96,7 @@ class NCSharePaging: UIViewController { private func setupHeader() { guard let headerView = Bundle.main.loadNibNamed("NCShareHeader", owner: self, options: nil)?.first as? NCShareHeader else { return } self.headerView = headerView - headerView.backgroundColor = .systemBackground + headerView.backgroundColor = .systemGroupedBackground headerView.setupUI(with: metadata) view.addSubview(headerView) @@ -119,7 +119,7 @@ class NCSharePaging: UIViewController { } ) let host = UIHostingController(rootView: content) - host.view.backgroundColor = .systemBackground + host.view.backgroundColor = .systemGroupedBackground addChild(host) view.addSubview(host.view) @@ -153,6 +153,7 @@ class NCSharePaging: UIViewController { viewController.metadata = metadata viewController.objectType = "files" viewController.account = metadata.account + viewController.usesGroupedBackground = true return viewController case .sharing: guard let viewController = UIStoryboard(name: "NCShare", bundle: nil).instantiateViewController(withIdentifier: "sharing") as? NCShare else { @@ -163,7 +164,7 @@ class NCSharePaging: UIViewController { viewController.controller = controller return viewController case .details: - return NCShareDetailsViewController() + return NCShareDetailsViewController(metadata: metadata) } } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index 23851fc7e0..3f9cf7b7fd 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -808,3 +808,11 @@ You can stop it at any time, adjust the settings, and enable it again."; "_e2ee_setup_store_privatekey_" = "Private key generation returned no usable key"; "_e2ee_setup_verify_publickey_" = "Public key does not match the certificate"; "_e2ee_setup_generate_passphrase_" = "Unable to generate a passphrase"; + +// Share details — Governance +"_governance_" = "Governance"; +"_sensitivity_label_" = "Sensitivity label"; +"_file_retention_" = "File retention"; +"_public_" = "Public"; +"_internal_use_only_" = "Internal use only"; +"_restricted_" = "Restricted"; From 61f6edfd92bd4a8f6d6cc5722097315c70a12bc5 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Mon, 29 Jun 2026 18:25:35 +0200 Subject: [PATCH 06/22] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 29 ++--- .../Share/NCShareDetailsGovernanceModel.swift | 107 +++++++++--------- iOSClient/Share/NCShareDetailsView.swift | 65 ++++++----- .../en.lproj/Localizable.strings | 4 +- 4 files changed, 108 insertions(+), 97 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index c66393ffc7..9a0878de07 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -4085,7 +4085,7 @@ F76DA967277B77E90082465B /* XCRemoteSwiftPackageReference "DropDown" */, F710FC78277B7CFF00AA9FBF /* XCRemoteSwiftPackageReference "realm-swift" */, F753BA91281FD8010015BFB6 /* XCRemoteSwiftPackageReference "EasyTipView" */, - F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */, + F783034028B511D200B84583 /* XCLocalSwiftPackageReference "../NextcloudKit-nc-governance" */, F734B06428E75C0100E180D5 /* XCRemoteSwiftPackageReference "TLPhotoPicker" */, F77333862927A72100466E35 /* XCRemoteSwiftPackageReference "OpenSSL" */, F77BC3E9293E5268005F2B08 /* XCRemoteSwiftPackageReference "swifter" */, @@ -6458,6 +6458,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + F783034028B511D200B84583 /* XCLocalSwiftPackageReference "../NextcloudKit-nc-governance" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../NextcloudKit-nc-governance"; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ F3374AEE2D78B01B002A38F9 /* XCRemoteSwiftPackageReference "swift-collections" */ = { isa = XCRemoteSwiftPackageReference; @@ -6587,14 +6594,6 @@ kind = branch; }; }; - F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/nextcloud/NextcloudKit"; - requirement = { - branch = main; - kind = branch; - }; - }; F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/bmoliveira/MarkdownKit"; @@ -6646,6 +6645,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + F7C55C9E2FB5B83A004A974F /* NextcloudKit */ = { + isa = XCSwiftPackageProductDependency; + productName = NextcloudKit; + }; F3374AEF2D78B01B002A38F9 /* BitCollections */ = { isa = XCSwiftPackageProductDependency; package = F3374AEE2D78B01B002A38F9 /* XCRemoteSwiftPackageReference "swift-collections" */; @@ -6743,7 +6746,6 @@ }; F37208B72BAB63EF006B5430 /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; - package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F37208B92BAB63EF006B5430 /* TLPhotoPicker */ = { @@ -6863,22 +6865,18 @@ }; F72AD70C28C24B93006CB92D /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; - package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F72AD70E28C24BA1006CB92D /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; - package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F72AD71028C24BBB006CB92D /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; - package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F72AD71228C24BCC006CB92D /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; - package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F72D7EB6263B1207000B3DFC /* MarkdownKit */ = { @@ -6903,7 +6901,6 @@ }; F7490E8A29882CE4009DCE94 /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; - package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F74C863C2AEFBFD9009A1D4A /* LRUCache */ = { @@ -6983,7 +6980,6 @@ }; F783034328B5142B00B84583 /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; - package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F788ECC6263AAAFA00ADC67F /* MarkdownKit */ = { @@ -7008,7 +7004,6 @@ }; F7A8D72328F1771B008BBE1C /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; - package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F7A8D72728F17728008BBE1C /* RealmSwift */ = { diff --git a/iOSClient/Share/NCShareDetailsGovernanceModel.swift b/iOSClient/Share/NCShareDetailsGovernanceModel.swift index 8b98a5a9d9..0c897eacd7 100644 --- a/iOSClient/Share/NCShareDetailsGovernanceModel.swift +++ b/iOSClient/Share/NCShareDetailsGovernanceModel.swift @@ -5,55 +5,26 @@ import Foundation import NextcloudKit +enum GovernanceViewState { + case loading + case selectedLabelsUpdated(entityLabels: NKGovernanceEntityLabels, availableSensitivityLabels: [NKGovernanceLabel], availableRetentionLabels: [NKGovernanceLabel]) + case error(Error) +} + @MainActor @Observable final class NCShareDetailsGovernanceModel { - enum SensitivityLabel: String, CaseIterable, Identifiable { - case publicLabel, internalUseOnly, restricted - - var id: String { rawValue } - - var localizedName: String { - switch self { - case .publicLabel: return NSLocalizedString("_public_", comment: "") - case .internalUseOnly: return NSLocalizedString("_internal_use_only_", comment: "") - case .restricted: return NSLocalizedString("_restricted_", comment: "") - } - } - - var systemImageName: String { - switch self { - case .publicLabel: return "link" - case .internalUseOnly: return "person.2" - case .restricted: return "xmark" - } - } - } - - enum RetentionPolicy: String, CaseIterable, Identifiable { - case publicPolicy, internalUseOnly, restricted + private(set) var sensitivityLabels: [NKGovernanceLabel] = [] + private(set) var retentionLabels: [NKGovernanceLabel] = [] +// private(set) var isLoading = false - var id: String { rawValue } + var state: GovernanceViewState = .loading - var localizedName: String { - switch self { - case .publicPolicy: return NSLocalizedString("_public_", comment: "") - case .internalUseOnly: return NSLocalizedString("_internal_use_only_", comment: "") - case .restricted: return NSLocalizedString("_restricted_", comment: "") - } - } + var selectedSensitivityLabelID = "" + var selectedRetentionLabelID = "" - var systemImageName: String { - switch self { - case .publicPolicy: return "link" - case .internalUseOnly: return "person.2" - case .restricted: return "xmark" - } - } - } - - var selectedSensitivityLabel: SensitivityLabel = .publicLabel - var selectedRetentionPolicy: RetentionPolicy = .publicPolicy + private var appliedSensitivityLabelID = "" + private var appliedRetentionLabelID = "" private let metadata: tableMetadata @@ -63,19 +34,53 @@ final class NCShareDetailsGovernanceModel { var account: String { metadata.account } - func getSensitivityLabels() -> [SensitivityLabel] { - SensitivityLabel.allCases + private var entityID: String { metadata.fileId } + + func load() async { + async let sensitivity = NextcloudKit.shared.getGovernanceAvailableSensitivityLabels(entityId: entityID, account: account) + async let retention = NextcloudKit.shared.getGovernanceAvailableRetentionLabels(entityId: entityID, account: account) + async let entity = NextcloudKit.shared.getGovernanceLabels(entityId: entityID, account: account) + + sensitivityLabels = await sensitivity.labels ?? [] + retentionLabels = await retention.labels ?? [] + + guard let entityLabels = await entity.labels, + let sensitivityLabels = await sensitivity.labels, + let retentionLabels = await retention.labels + else { return } +// appliedSensitivityLabelID = entityLabels?.sensitivity?.id ?? "" +// appliedRetentionLabelID = entityLabels?.retention.first?.id ?? "" +// selectedSensitivityLabelID = appliedSensitivityLabelID +// selectedRetentionLabelID = appliedRetentionLabelID + + state = .selectedLabelsUpdated(entityLabels: entityLabels, availableSensitivityLabels: sensitivityLabels, availableRetentionLabels: retentionLabels) } - func getRetentionPolicies() -> [RetentionPolicy] { - RetentionPolicy.allCases + func applySensitivityLabel(_ id: String) async { + guard id != appliedSensitivityLabelID else { return } + + if await applyLabel(type: .sensitivity, newID: id, appliedID: appliedSensitivityLabelID) { + appliedSensitivityLabelID = id + } else { + selectedSensitivityLabelID = appliedSensitivityLabelID + } } - func setSensitivityLabel(_ label: SensitivityLabel) { - selectedSensitivityLabel = label + func applyRetentionLabel(_ id: String) async { + guard id != appliedRetentionLabelID else { return } + + if await applyLabel(type: .retention, newID: id, appliedID: appliedRetentionLabelID) { + appliedRetentionLabelID = id + } else { + selectedRetentionLabelID = appliedRetentionLabelID + } } - func setRetentionPolicy(_ policy: RetentionPolicy) { - selectedRetentionPolicy = policy + private func applyLabel(type: NKGovernanceLabelType, newID: String, appliedID: String) async -> Bool { + if newID.isEmpty { + return await NextcloudKit.shared.removeGovernanceLabel(entityId: entityID, labelType: type, labelId: appliedID, account: account).error == .success + } + + return await NextcloudKit.shared.setGovernanceLabel(entityId: entityID, labelType: type, labelId: newID, account: account).error == .success } } diff --git a/iOSClient/Share/NCShareDetailsView.swift b/iOSClient/Share/NCShareDetailsView.swift index 9f3a8f365c..98efa3cf74 100644 --- a/iOSClient/Share/NCShareDetailsView.swift +++ b/iOSClient/Share/NCShareDetailsView.swift @@ -4,6 +4,7 @@ import SwiftUI import UIKit +import NextcloudKit struct NCShareDetailsView: View { @State private var model: NCShareDetailsGovernanceModel @@ -17,38 +18,50 @@ struct NCShareDetailsView: View { Section(header: Text(NSLocalizedString("_governance_", comment: "")).font(.headline) ) { - HStack { - Text(NSLocalizedString("_sensitivity_label_", comment: "")) - .cappedFont(.body, maxDynamicType: .accessibility2) - Spacer() - Picker("", selection: $model.selectedSensitivityLabel) { - ForEach(model.getSensitivityLabels()) { label in - Label(label.localizedName, systemImage: label.systemImageName).tag(label) - } - } - .pickerStyle(.menu) - } - .onChange(of: model.selectedSensitivityLabel) { _, newValue in - model.setSensitivityLabel(newValue) + labelPicker( + title: "_sensitivity_label_", + labels: model.sensitivityLabels, + selection: $model.selectedSensitivityLabelID + ) { newValue in + await model.applySensitivityLabel(newValue) } - HStack { - Text(NSLocalizedString("_file_retention_", comment: "")) - .cappedFont(.body, maxDynamicType: .accessibility2) - Spacer() - Picker("", selection: $model.selectedRetentionPolicy) { - ForEach(model.getRetentionPolicies()) { policy in - Label(policy.localizedName, systemImage: policy.systemImageName).tag(policy) - } - } - .pickerStyle(.menu) - } - .onChange(of: model.selectedRetentionPolicy) { _, newValue in - model.setRetentionPolicy(newValue) + labelPicker( + title: "_file_retention_", + labels: model.retentionLabels, + selection: $model.selectedRetentionLabelID + ) { newValue in + await model.applyRetentionLabel(newValue) } } } .tint(Color(NCBrandColor.shared.getElement(account: model.account))) + .task { + await model.load() + } + } + + private func labelPicker( + title: String, + labels: [NKGovernanceLabel], + selection: Binding, + onChange: @escaping (String) async -> Void + ) -> some View { + HStack { + Text(NSLocalizedString(title, comment: "")) + .cappedFont(.body, maxDynamicType: .accessibility2) + Spacer() + Picker("", selection: selection) { + Text(NSLocalizedString("_none_", comment: "")).tag("") + ForEach(labels, id: \.id) { label in + Text(label.name).tag(label.id) + } + } + .pickerStyle(.menu) + } + .onChange(of: selection.wrappedValue) { _, newValue in + Task { await onChange(newValue) } + } } } diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index a9530ca70d..d0140b5658 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -827,6 +827,4 @@ You can stop it at any time, adjust the settings, and enable it again."; "_governance_" = "Governance"; "_sensitivity_label_" = "Sensitivity label"; "_file_retention_" = "File retention"; -"_public_" = "Public"; -"_internal_use_only_" = "Internal use only"; -"_restricted_" = "Restricted"; +"_none_" = "None"; From 50c342a800a2825d5493f5affdb37f7038f6c91d Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 30 Jun 2026 18:11:25 +0200 Subject: [PATCH 07/22] WIP Signed-off-by: Milen Pivchev --- .../Share/NCShareDetailsGovernanceModel.swift | 61 +++++++------------ iOSClient/Share/NCShareDetailsView.swift | 54 +++++++++------- 2 files changed, 55 insertions(+), 60 deletions(-) diff --git a/iOSClient/Share/NCShareDetailsGovernanceModel.swift b/iOSClient/Share/NCShareDetailsGovernanceModel.swift index 0c897eacd7..8671b6deda 100644 --- a/iOSClient/Share/NCShareDetailsGovernanceModel.swift +++ b/iOSClient/Share/NCShareDetailsGovernanceModel.swift @@ -5,27 +5,25 @@ import Foundation import NextcloudKit +struct GovernanceData { + let availableSensitivityLabels: [NKGovernanceLabel] + let availableRetentionLabels: [NKGovernanceLabel] +} + enum GovernanceViewState { case loading - case selectedLabelsUpdated(entityLabels: NKGovernanceEntityLabels, availableSensitivityLabels: [NKGovernanceLabel], availableRetentionLabels: [NKGovernanceLabel]) + case loaded(GovernanceData) case error(Error) } @MainActor @Observable final class NCShareDetailsGovernanceModel { - private(set) var sensitivityLabels: [NKGovernanceLabel] = [] - private(set) var retentionLabels: [NKGovernanceLabel] = [] -// private(set) var isLoading = false - var state: GovernanceViewState = .loading var selectedSensitivityLabelID = "" var selectedRetentionLabelID = "" - private var appliedSensitivityLabelID = "" - private var appliedRetentionLabelID = "" - private let metadata: tableMetadata init(metadata: tableMetadata) { @@ -41,46 +39,33 @@ final class NCShareDetailsGovernanceModel { async let retention = NextcloudKit.shared.getGovernanceAvailableRetentionLabels(entityId: entityID, account: account) async let entity = NextcloudKit.shared.getGovernanceLabels(entityId: entityID, account: account) - sensitivityLabels = await sensitivity.labels ?? [] - retentionLabels = await retention.labels ?? [] - guard let entityLabels = await entity.labels, - let sensitivityLabels = await sensitivity.labels, - let retentionLabels = await retention.labels + let sensitivityLabels = await sensitivity.labels, + let retentionLabels = await retention.labels else { return } -// appliedSensitivityLabelID = entityLabels?.sensitivity?.id ?? "" -// appliedRetentionLabelID = entityLabels?.retention.first?.id ?? "" -// selectedSensitivityLabelID = appliedSensitivityLabelID -// selectedRetentionLabelID = appliedRetentionLabelID - - state = .selectedLabelsUpdated(entityLabels: entityLabels, availableSensitivityLabels: sensitivityLabels, availableRetentionLabels: retentionLabels) - } - func applySensitivityLabel(_ id: String) async { - guard id != appliedSensitivityLabelID else { return } + selectedSensitivityLabelID = entityLabels.sensitivity?.id ?? "" + selectedRetentionLabelID = entityLabels.retention.first?.id ?? "" - if await applyLabel(type: .sensitivity, newID: id, appliedID: appliedSensitivityLabelID) { - appliedSensitivityLabelID = id - } else { - selectedSensitivityLabelID = appliedSensitivityLabelID - } + state = .loaded(GovernanceData( + availableSensitivityLabels: sensitivityLabels, + availableRetentionLabels: retentionLabels + )) } - func applyRetentionLabel(_ id: String) async { - guard id != appliedRetentionLabelID else { return } + func applySensitivityLabel(from oldID: String, to newID: String) async { + await applyLabel(type: .sensitivity, oldID: oldID, newID: newID) + } - if await applyLabel(type: .retention, newID: id, appliedID: appliedRetentionLabelID) { - appliedRetentionLabelID = id - } else { - selectedRetentionLabelID = appliedRetentionLabelID - } + func applyRetentionLabel(from oldID: String, to newID: String) async { + await applyLabel(type: .retention, oldID: oldID, newID: newID) } - private func applyLabel(type: NKGovernanceLabelType, newID: String, appliedID: String) async -> Bool { + private func applyLabel(type: NKGovernanceLabelType, oldID: String, newID: String) async { if newID.isEmpty { - return await NextcloudKit.shared.removeGovernanceLabel(entityId: entityID, labelType: type, labelId: appliedID, account: account).error == .success + _ = await NextcloudKit.shared.removeGovernanceLabel(entityId: entityID, labelType: type, labelId: oldID, account: account) + } else { + _ = await NextcloudKit.shared.setGovernanceLabel(entityId: entityID, labelType: type, labelId: newID, account: account) } - - return await NextcloudKit.shared.setGovernanceLabel(entityId: entityID, labelType: type, labelId: newID, account: account).error == .success } } diff --git a/iOSClient/Share/NCShareDetailsView.swift b/iOSClient/Share/NCShareDetailsView.swift index 98efa3cf74..dd97434a8d 100644 --- a/iOSClient/Share/NCShareDetailsView.swift +++ b/iOSClient/Share/NCShareDetailsView.swift @@ -14,28 +14,38 @@ struct NCShareDetailsView: View { } var body: some View { - Form { - Section(header: - Text(NSLocalizedString("_governance_", comment: "")).font(.headline) - ) { - labelPicker( - title: "_sensitivity_label_", - labels: model.sensitivityLabels, - selection: $model.selectedSensitivityLabelID - ) { newValue in - await model.applySensitivityLabel(newValue) - } + ZStack { + switch model.state { + case .loading: + Text("Loading") + case .loaded(let data): + Form { + Section(header: + Text(NSLocalizedString("_governance_", comment: "")).font(.headline) + ) { + labelPicker( + title: "_sensitivity_label_", + labels: data.availableSensitivityLabels, + selection: $model.selectedSensitivityLabelID + ) { oldValue, newValue in + await model.applySensitivityLabel(from: oldValue, to: newValue) + } - labelPicker( - title: "_file_retention_", - labels: model.retentionLabels, - selection: $model.selectedRetentionLabelID - ) { newValue in - await model.applyRetentionLabel(newValue) - } + labelPicker( + title: "_file_retention_", + labels: data.availableRetentionLabels, + selection: $model.selectedRetentionLabelID + ) { oldValue, newValue in + await model.applyRetentionLabel(from: oldValue, to: newValue) + } + } + } + .tint(Color(NCBrandColor.shared.getElement(account: model.account))) + + case .error(let error): + Text(error.localizedDescription) } } - .tint(Color(NCBrandColor.shared.getElement(account: model.account))) .task { await model.load() } @@ -45,7 +55,7 @@ struct NCShareDetailsView: View { title: String, labels: [NKGovernanceLabel], selection: Binding, - onChange: @escaping (String) async -> Void + onChange: @escaping (_ oldValue: String, _ newValue: String) async -> Void ) -> some View { HStack { Text(NSLocalizedString(title, comment: "")) @@ -59,8 +69,8 @@ struct NCShareDetailsView: View { } .pickerStyle(.menu) } - .onChange(of: selection.wrappedValue) { _, newValue in - Task { await onChange(newValue) } + .onChange(of: selection.wrappedValue) { oldValue, newValue in + Task { await onChange(oldValue, newValue) } } } } From 0c56f5bfafcd5ec4653ea137a0349c6946bc2114 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 1 Jul 2026 19:04:20 +0200 Subject: [PATCH 08/22] WIP Signed-off-by: Milen Pivchev --- .../Share/NCShareDetailsGovernanceModel.swift | 14 ++++++++++++-- iOSClient/Share/NCShareDetailsView.swift | 8 ++++++++ .../Supporting Files/en.lproj/Localizable.strings | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/iOSClient/Share/NCShareDetailsGovernanceModel.swift b/iOSClient/Share/NCShareDetailsGovernanceModel.swift index 8671b6deda..cf1acacfcc 100644 --- a/iOSClient/Share/NCShareDetailsGovernanceModel.swift +++ b/iOSClient/Share/NCShareDetailsGovernanceModel.swift @@ -8,6 +8,7 @@ import NextcloudKit struct GovernanceData { let availableSensitivityLabels: [NKGovernanceLabel] let availableRetentionLabels: [NKGovernanceLabel] + let availableHoldLabels: [NKGovernanceLabel] } enum GovernanceViewState { @@ -23,6 +24,7 @@ final class NCShareDetailsGovernanceModel { var selectedSensitivityLabelID = "" var selectedRetentionLabelID = "" + var selectedHoldLabelID = "" private let metadata: tableMetadata @@ -37,19 +39,23 @@ final class NCShareDetailsGovernanceModel { func load() async { async let sensitivity = NextcloudKit.shared.getGovernanceAvailableSensitivityLabels(entityId: entityID, account: account) async let retention = NextcloudKit.shared.getGovernanceAvailableRetentionLabels(entityId: entityID, account: account) + async let hold = NextcloudKit.shared.getGovernanceAvailableHoldLabels(entityId: entityID, account: account) async let entity = NextcloudKit.shared.getGovernanceLabels(entityId: entityID, account: account) guard let entityLabels = await entity.labels, let sensitivityLabels = await sensitivity.labels, - let retentionLabels = await retention.labels + let retentionLabels = await retention.labels, + let holdLabels = await hold.labels else { return } selectedSensitivityLabelID = entityLabels.sensitivity?.id ?? "" selectedRetentionLabelID = entityLabels.retention.first?.id ?? "" + // NKGovernanceEntityLabels exposes no hold, so the applied hold can't be preselected. state = .loaded(GovernanceData( availableSensitivityLabels: sensitivityLabels, - availableRetentionLabels: retentionLabels + availableRetentionLabels: retentionLabels, + availableHoldLabels: holdLabels )) } @@ -61,6 +67,10 @@ final class NCShareDetailsGovernanceModel { await applyLabel(type: .retention, oldID: oldID, newID: newID) } + func applyHoldLabel(from oldID: String, to newID: String) async { + await applyLabel(type: .hold, oldID: oldID, newID: newID) + } + private func applyLabel(type: NKGovernanceLabelType, oldID: String, newID: String) async { if newID.isEmpty { _ = await NextcloudKit.shared.removeGovernanceLabel(entityId: entityID, labelType: type, labelId: oldID, account: account) diff --git a/iOSClient/Share/NCShareDetailsView.swift b/iOSClient/Share/NCShareDetailsView.swift index dd97434a8d..a33821a421 100644 --- a/iOSClient/Share/NCShareDetailsView.swift +++ b/iOSClient/Share/NCShareDetailsView.swift @@ -38,6 +38,14 @@ struct NCShareDetailsView: View { ) { oldValue, newValue in await model.applyRetentionLabel(from: oldValue, to: newValue) } + + labelPicker( + title: "_legal_hold_", + labels: data.availableHoldLabels, + selection: $model.selectedHoldLabelID + ) { oldValue, newValue in + await model.applyHoldLabel(from: oldValue, to: newValue) + } } } .tint(Color(NCBrandColor.shared.getElement(account: model.account))) diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index d0140b5658..a098c36eb7 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -827,4 +827,5 @@ You can stop it at any time, adjust the settings, and enable it again."; "_governance_" = "Governance"; "_sensitivity_label_" = "Sensitivity label"; "_file_retention_" = "File retention"; +"_legal_hold_" = "Legal hold"; "_none_" = "None"; From 67ab823ef15ae828b4d1f5749fbf40aba4aa5da9 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 2 Jul 2026 10:19:20 +0200 Subject: [PATCH 09/22] WIP Signed-off-by: Milen Pivchev --- iOSClient/Share/NCShareHeader.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/iOSClient/Share/NCShareHeader.swift b/iOSClient/Share/NCShareHeader.swift index ea3fa20a9e..e0a611930c 100644 --- a/iOSClient/Share/NCShareHeader.swift +++ b/iOSClient/Share/NCShareHeader.swift @@ -70,6 +70,7 @@ class NCShareHeader: UIView { info.text = utilityFileSystem.transformedSize(metadata.size) + ", " + NCUtility().getRelativeDateTitle(metadata.date as Date) tagListView.marginY = 8 + tagListView.backgroundColor = .secondarySystemBackground refreshTags(metadata.tagNames, tagModels: metadata.tags.map(\.nkTag)) setNeedsLayout() From 3d9bd13cd4464d375b0677b0499177f8b18a51ac Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 2 Jul 2026 14:30:07 +0200 Subject: [PATCH 10/22] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 4 + .../Share/NCGovernanceLabelSelectorView.swift | 105 ++++++++++++++++++ .../Share/NCShareDetailsGovernanceModel.swift | 29 +++-- iOSClient/Share/NCShareDetailsView.swift | 73 ++++++++++-- 4 files changed, 194 insertions(+), 17 deletions(-) create mode 100644 iOSClient/Share/NCGovernanceLabelSelectorView.swift diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 9a0878de07..3f528695bc 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ AB6000032F60000200FE2775 /* NCTagEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6000022F60000200FE2775 /* NCTagEditorView.swift */; }; AB7000022F70000100FE2776 /* NCShareDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7000012F70000100FE2776 /* NCShareDetailsView.swift */; }; AB7000042F70000100FE2776 /* NCShareDetailsGovernanceModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7000032F70000100FE2776 /* NCShareDetailsGovernanceModel.swift */; }; + AB7000062F70000100FE2776 /* NCGovernanceLabelSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7000052F70000100FE2776 /* NCGovernanceLabelSelectorView.swift */; }; AF1A9B6427D0CA1E00F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF1A9B6527D0CC0500F17A9E /* UIAlertController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */; }; AF22B206277B4E4C00DAB0CC /* NCCreateFormUploadConflict.swift in Sources */ = {isa = PBXBuildFile; fileRef = F704B5E42430AA8000632F5F /* NCCreateFormUploadConflict.swift */; }; @@ -1289,6 +1290,7 @@ AB6000022F60000200FE2775 /* NCTagEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCTagEditorView.swift; sourceTree = ""; }; AB7000012F70000100FE2776 /* NCShareDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareDetailsView.swift; sourceTree = ""; }; AB7000032F70000100FE2776 /* NCShareDetailsGovernanceModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCShareDetailsGovernanceModel.swift; sourceTree = ""; }; + AB7000052F70000100FE2776 /* NCGovernanceLabelSelectorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCGovernanceLabelSelectorView.swift; sourceTree = ""; }; AF1A9B6327D0CA1E00F17A9E /* UIAlertController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Extension.swift"; sourceTree = ""; }; AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareCell.swift; sourceTree = ""; }; AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+DataSource.swift"; sourceTree = ""; }; @@ -2503,6 +2505,7 @@ AF93471527E2361E002537EE /* NCShareHeader.swift */, AB7000012F70000100FE2776 /* NCShareDetailsView.swift */, AB7000032F70000100FE2776 /* NCShareDetailsGovernanceModel.swift */, + AB7000052F70000100FE2776 /* NCGovernanceLabelSelectorView.swift */, AFCE353827E5DE0400FEA6C2 /* Shareable.swift */, AA8E03D92D2ED83300E7E89C /* TransientShare.swift */, ); @@ -4784,6 +4787,7 @@ F769454022E9F077000A798A /* NCSharePaging.swift in Sources */, AB7000022F70000100FE2776 /* NCShareDetailsView.swift in Sources */, AB7000042F70000100FE2776 /* NCShareDetailsGovernanceModel.swift in Sources */, + AB7000062F70000100FE2776 /* NCGovernanceLabelSelectorView.swift in Sources */, F7C687E92D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */, F74D50352C9855A000BBBF4C /* NCCollectionViewCommon+CollectionViewDataSourcePrefetching.swift in Sources */, F7802B322BD5584F00D74270 /* NCMedia+DragDrop.swift in Sources */, diff --git a/iOSClient/Share/NCGovernanceLabelSelectorView.swift b/iOSClient/Share/NCGovernanceLabelSelectorView.swift new file mode 100644 index 0000000000..1e7447c3f5 --- /dev/null +++ b/iOSClient/Share/NCGovernanceLabelSelectorView.swift @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2026 Milen Pivchev +// SPDX-License-Identifier: GPL-3.0-or-later + +import SwiftUI +import UIKit +import NextcloudKit + +/// Reusable multi-select sheet for a governance label type (retention / hold). +/// Holds the selection transiently and reports the chosen set on Save. +struct NCGovernanceLabelSelectorView: View { + let title: String + let labels: [NKGovernanceLabel] + let account: String + let onSave: (Set) async -> Void + + @State private var selected: Set + @State private var isSaving = false + @Environment(\.dismiss) private var dismiss + + init( + title: String, + labels: [NKGovernanceLabel], + account: String, + initialSelection: Set, + onSave: @escaping (Set) async -> Void + ) { + self.title = title + self.labels = labels + self.account = account + self.onSave = onSave + _selected = State(initialValue: initialSelection) + } + + var body: some View { + NavigationStack { + List { + ForEach(labels, id: \.id) { label in + Button { + toggle(label.id) + } label: { + HStack { + Circle() + .fill(color(for: label)) + .frame(width: 10, height: 10) + + Text(label.name) + .foregroundStyle(.primary) + + Spacer() + + if selected.contains(label.id) { + Image(systemName: "checkmark") + .foregroundStyle(Color(NCBrandColor.shared.getElement(account: account))) + } + } + } + } + } + .navigationTitle(NSLocalizedString(title, comment: "")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(NSLocalizedString("_cancel_", comment: "")) { + dismiss() + } + } + + ToolbarItem(placement: .confirmationAction) { + Button(NSLocalizedString("_save_", comment: "")) { + Task { + isSaving = true + await onSave(selected) + isSaving = false + dismiss() + } + } + } + } + .overlay { + if isSaving { + ProgressView() + } + } + .disabled(isSaving) + .tint(Color(NCBrandColor.shared.getElement(account: account))) + } + } + + private func toggle(_ id: String) { + if selected.contains(id) { + selected.remove(id) + } else { + selected.insert(id) + } + } + + private func color(for label: NKGovernanceLabel) -> Color { + if let color = UIColor(hex: label.color) { + return Color(color) + } + + return .secondary + } +} diff --git a/iOSClient/Share/NCShareDetailsGovernanceModel.swift b/iOSClient/Share/NCShareDetailsGovernanceModel.swift index cf1acacfcc..7cee2348c9 100644 --- a/iOSClient/Share/NCShareDetailsGovernanceModel.swift +++ b/iOSClient/Share/NCShareDetailsGovernanceModel.swift @@ -23,8 +23,8 @@ final class NCShareDetailsGovernanceModel { var state: GovernanceViewState = .loading var selectedSensitivityLabelID = "" - var selectedRetentionLabelID = "" - var selectedHoldLabelID = "" + var selectedRetentionLabelIDs: Set = [] + var selectedHoldLabelIDs: Set = [] private let metadata: tableMetadata @@ -49,8 +49,8 @@ final class NCShareDetailsGovernanceModel { else { return } selectedSensitivityLabelID = entityLabels.sensitivity?.id ?? "" - selectedRetentionLabelID = entityLabels.retention.first?.id ?? "" - // NKGovernanceEntityLabels exposes no hold, so the applied hold can't be preselected. + selectedRetentionLabelIDs = Set(entityLabels.retention.map(\.id)) + // NKGovernanceEntityLabels exposes no hold, so applied holds can't be preselected. state = .loaded(GovernanceData( availableSensitivityLabels: sensitivityLabels, @@ -63,12 +63,23 @@ final class NCShareDetailsGovernanceModel { await applyLabel(type: .sensitivity, oldID: oldID, newID: newID) } - func applyRetentionLabel(from oldID: String, to newID: String) async { - await applyLabel(type: .retention, oldID: oldID, newID: newID) - } + /// Persists a multi-selection by diffing against what's applied and calling set/remove per changed label. + func saveLabels(type: NKGovernanceLabelType, selectedIDs: Set) async { + let current = (type == .retention) ? selectedRetentionLabelIDs : selectedHoldLabelIDs + + for id in selectedIDs.subtracting(current) { + await applyLabel(type: type, oldID: "", newID: id) + } - func applyHoldLabel(from oldID: String, to newID: String) async { - await applyLabel(type: .hold, oldID: oldID, newID: newID) + for id in current.subtracting(selectedIDs) { + await applyLabel(type: type, oldID: id, newID: "") + } + + if type == .retention { + selectedRetentionLabelIDs = selectedIDs + } else { + selectedHoldLabelIDs = selectedIDs + } } private func applyLabel(type: NKGovernanceLabelType, oldID: String, newID: String) async { diff --git a/iOSClient/Share/NCShareDetailsView.swift b/iOSClient/Share/NCShareDetailsView.swift index a33821a421..9f1683358a 100644 --- a/iOSClient/Share/NCShareDetailsView.swift +++ b/iOSClient/Share/NCShareDetailsView.swift @@ -8,6 +8,14 @@ import NextcloudKit struct NCShareDetailsView: View { @State private var model: NCShareDetailsGovernanceModel + @State private var activeSelector: GovernanceSelector? + + private enum GovernanceSelector: String, Identifiable { + case retention + case hold + + var id: String { rawValue } + } init(metadata: tableMetadata) { _model = State(initialValue: NCShareDetailsGovernanceModel(metadata: metadata)) @@ -31,24 +39,37 @@ struct NCShareDetailsView: View { await model.applySensitivityLabel(from: oldValue, to: newValue) } - labelPicker( + labelSelectorRow( title: "_file_retention_", labels: data.availableRetentionLabels, - selection: $model.selectedRetentionLabelID - ) { oldValue, newValue in - await model.applyRetentionLabel(from: oldValue, to: newValue) + selectedIDs: model.selectedRetentionLabelIDs + ) { + activeSelector = .retention } - labelPicker( + labelSelectorRow( title: "_legal_hold_", labels: data.availableHoldLabels, - selection: $model.selectedHoldLabelID - ) { oldValue, newValue in - await model.applyHoldLabel(from: oldValue, to: newValue) + selectedIDs: model.selectedHoldLabelIDs + ) { + activeSelector = .hold } } } .tint(Color(NCBrandColor.shared.getElement(account: model.account))) + .sheet(item: $activeSelector) { selector in + let isRetention = selector == .retention + + NCGovernanceLabelSelectorView( + title: isRetention ? "_file_retention_" : "_legal_hold_", + labels: isRetention ? data.availableRetentionLabels : data.availableHoldLabels, + account: model.account, + initialSelection: isRetention ? model.selectedRetentionLabelIDs : model.selectedHoldLabelIDs + ) { newSelection in + await model.saveLabels(type: isRetention ? .retention : .hold, selectedIDs: newSelection) + } + .presentationDetents([.medium, .large]) + } case .error(let error): Text(error.localizedDescription) @@ -81,6 +102,42 @@ struct NCShareDetailsView: View { Task { await onChange(oldValue, newValue) } } } + + private func labelSelectorRow( + title: String, + labels: [NKGovernanceLabel], + selectedIDs: Set, + action: @escaping () -> Void + ) -> some View { + Button(action: action) { + HStack { + Text(NSLocalizedString(title, comment: "")) + .cappedFont(.body, maxDynamicType: .accessibility2) + .foregroundStyle(.primary) + + Spacer() + + Text(summary(labels: labels, selectedIDs: selectedIDs)) + .foregroundStyle(.secondary) + .lineLimit(1) + + Image(systemName: "chevron.right") + .font(.footnote.weight(.semibold)) + .foregroundStyle(.tertiary) + } + } + } + + private func summary(labels: [NKGovernanceLabel], selectedIDs: Set) -> String { + guard !selectedIDs.isEmpty else { + return NSLocalizedString("_none_", comment: "") + } + + return labels + .filter { selectedIDs.contains($0.id) } + .map(\.name) + .joined(separator: ", ") + } } final class NCShareDetailsViewController: UIHostingController, NCSharePagingContent { From 79320edfc201151da468100f7aa458c4b65ace49 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 14 Jul 2026 16:59:59 +0200 Subject: [PATCH 11/22] WIP Signed-off-by: Milen Pivchev --- iOSClient/Share/NCGovernanceLabelSelectorView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iOSClient/Share/NCGovernanceLabelSelectorView.swift b/iOSClient/Share/NCGovernanceLabelSelectorView.swift index 1e7447c3f5..0a02c151c9 100644 --- a/iOSClient/Share/NCGovernanceLabelSelectorView.swift +++ b/iOSClient/Share/NCGovernanceLabelSelectorView.swift @@ -61,13 +61,13 @@ struct NCGovernanceLabelSelectorView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { - Button(NSLocalizedString("_cancel_", comment: "")) { + Button(NSLocalizedString("_cancel_", comment: ""), systemImage: "xmark") { dismiss() } } ToolbarItem(placement: .confirmationAction) { - Button(NSLocalizedString("_save_", comment: "")) { + Button(NSLocalizedString("_save_", comment: ""), systemImage: "checkmark.circle.fill") { Task { isSaving = true await onSave(selected) From 8c13e716e231e4f0fea8629a7400cbafa855fa4d Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 15 Jul 2026 18:30:05 +0200 Subject: [PATCH 12/22] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 8 +- iOSClient/Main/Create/NCCreate.swift | 3 + .../Share/NCGovernanceLabelSelectorView.swift | 59 +++++++++---- .../Share/NCShareDetailsGovernanceModel.swift | 28 +++--- iOSClient/Share/NCShareDetailsView.swift | 87 +++++++++++-------- 5 files changed, 119 insertions(+), 66 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 8f15861550..dbc408767a 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -6630,10 +6630,6 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - F7C55C9E2FB5B83A004A974F /* NextcloudKit */ = { - isa = XCSwiftPackageProductDependency; - productName = NextcloudKit; - }; F3374AEF2D78B01B002A38F9 /* BitCollections */ = { isa = XCSwiftPackageProductDependency; package = F3374AEE2D78B01B002A38F9 /* XCRemoteSwiftPackageReference "swift-collections" */; @@ -6961,6 +6957,10 @@ package = F710FC78277B7CFF00AA9FBF /* XCRemoteSwiftPackageReference "realm-swift" */; productName = RealmSwift; }; + F7C55C9E2FB5B83A004A974F /* NextcloudKit */ = { + isa = XCSwiftPackageProductDependency; + productName = NextcloudKit; + }; F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */ = { isa = XCSwiftPackageProductDependency; package = F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */; diff --git a/iOSClient/Main/Create/NCCreate.swift b/iOSClient/Main/Create/NCCreate.swift index a0ac4ea192..f7059b47b3 100644 --- a/iOSClient/Main/Create/NCCreate.swift +++ b/iOSClient/Main/Create/NCCreate.swift @@ -195,6 +195,9 @@ class NCCreate: NSObject { if !metadata.isSharable(), let idx = pages.firstIndex(of: .sharing) { pages.remove(at: idx) } + if !capabilities.governanceEnabled, let idx = pages.firstIndex(of: .details) { + pages.remove(at: idx) + } shareViewController?.pages = pages shareViewController?.metadata = metadata diff --git a/iOSClient/Share/NCGovernanceLabelSelectorView.swift b/iOSClient/Share/NCGovernanceLabelSelectorView.swift index 0a02c151c9..a7023db93e 100644 --- a/iOSClient/Share/NCGovernanceLabelSelectorView.swift +++ b/iOSClient/Share/NCGovernanceLabelSelectorView.swift @@ -6,12 +6,14 @@ import SwiftUI import UIKit import NextcloudKit -/// Reusable multi-select sheet for a governance label type (retention / hold). -/// Holds the selection transiently and reports the chosen set on Save. +/// Reusable selection sheet for a governance label type. +/// Single-select (sensitivity, with a "None" row) or multi-select (retention / hold); +/// holds the selection transiently and reports the chosen set on Save. struct NCGovernanceLabelSelectorView: View { let title: String let labels: [NKGovernanceLabel] let account: String + let allowsMultipleSelection: Bool let onSave: (Set) async -> Void @State private var selected: Set @@ -22,12 +24,14 @@ struct NCGovernanceLabelSelectorView: View { title: String, labels: [NKGovernanceLabel], account: String, + allowsMultipleSelection: Bool = true, initialSelection: Set, onSave: @escaping (Set) async -> Void ) { self.title = title self.labels = labels self.account = account + self.allowsMultipleSelection = allowsMultipleSelection self.onSave = onSave _selected = State(initialValue: initialSelection) } @@ -35,25 +39,15 @@ struct NCGovernanceLabelSelectorView: View { var body: some View { NavigationStack { List { + if !allowsMultipleSelection { + noneRow + } + ForEach(labels, id: \.id) { label in Button { toggle(label.id) } label: { - HStack { - Circle() - .fill(color(for: label)) - .frame(width: 10, height: 10) - - Text(label.name) - .foregroundStyle(.primary) - - Spacer() - - if selected.contains(label.id) { - Image(systemName: "checkmark") - .foregroundStyle(Color(NCBrandColor.shared.getElement(account: account))) - } - } + row(dot: color(for: label), name: label.name, isSelected: selected.contains(label.id)) } } } @@ -87,7 +81,38 @@ struct NCGovernanceLabelSelectorView: View { } } + private var noneRow: some View { + Button { + selected = [] + } label: { + row(dot: .clear, name: NSLocalizedString("_none_", comment: ""), isSelected: selected.isEmpty) + } + } + + private func row(dot: Color, name: String, isSelected: Bool) -> some View { + HStack { + Circle() + .fill(dot) + .frame(width: 10, height: 10) + + Text(name) + .foregroundStyle(.primary) + + Spacer() + + if isSelected { + Image(systemName: "checkmark") + .foregroundStyle(Color(NCBrandColor.shared.getElement(account: account))) + } + } + } + private func toggle(_ id: String) { + if !allowsMultipleSelection { + selected = [id] + return + } + if selected.contains(id) { selected.remove(id) } else { diff --git a/iOSClient/Share/NCShareDetailsGovernanceModel.swift b/iOSClient/Share/NCShareDetailsGovernanceModel.swift index 7cee2348c9..fd335cf0f5 100644 --- a/iOSClient/Share/NCShareDetailsGovernanceModel.swift +++ b/iOSClient/Share/NCShareDetailsGovernanceModel.swift @@ -42,25 +42,31 @@ final class NCShareDetailsGovernanceModel { async let hold = NextcloudKit.shared.getGovernanceAvailableHoldLabels(entityId: entityID, account: account) async let entity = NextcloudKit.shared.getGovernanceLabels(entityId: entityID, account: account) - guard let entityLabels = await entity.labels, - let sensitivityLabels = await sensitivity.labels, - let retentionLabels = await retention.labels, - let holdLabels = await hold.labels - else { return } + let entityResult = await entity + let sensitivityLabels = await sensitivity.labels + let retentionLabels = await retention.labels + let holdLabels = await hold.labels + + if entityResult.labels == nil, sensitivityLabels == nil, retentionLabels == nil, holdLabels == nil { + state = .error(entityResult.error) + return + } - selectedSensitivityLabelID = entityLabels.sensitivity?.id ?? "" - selectedRetentionLabelIDs = Set(entityLabels.retention.map(\.id)) - // NKGovernanceEntityLabels exposes no hold, so applied holds can't be preselected. + let entityLabels = entityResult.labels + selectedSensitivityLabelID = entityLabels?.sensitivity?.id ?? "" + selectedRetentionLabelIDs = Set(entityLabels?.retention.map(\.id) ?? []) + selectedHoldLabelIDs = Set(entityLabels?.hold.map(\.id) ?? []) state = .loaded(GovernanceData( - availableSensitivityLabels: sensitivityLabels, - availableRetentionLabels: retentionLabels, - availableHoldLabels: holdLabels + availableSensitivityLabels: sensitivityLabels ?? [], + availableRetentionLabels: retentionLabels ?? [], + availableHoldLabels: holdLabels ?? [] )) } func applySensitivityLabel(from oldID: String, to newID: String) async { await applyLabel(type: .sensitivity, oldID: oldID, newID: newID) + selectedSensitivityLabelID = newID } /// Persists a multi-selection by diffing against what's applied and calling set/remove per changed label. diff --git a/iOSClient/Share/NCShareDetailsView.swift b/iOSClient/Share/NCShareDetailsView.swift index 9f1683358a..2466c134fe 100644 --- a/iOSClient/Share/NCShareDetailsView.swift +++ b/iOSClient/Share/NCShareDetailsView.swift @@ -11,10 +11,19 @@ struct NCShareDetailsView: View { @State private var activeSelector: GovernanceSelector? private enum GovernanceSelector: String, Identifiable { + case sensitivity case retention case hold var id: String { rawValue } + + var title: String { + switch self { + case .sensitivity: return "_sensitivity_label_" + case .retention: return "_file_retention_" + case .hold: return "_legal_hold_" + } + } } init(metadata: tableMetadata) { @@ -31,12 +40,12 @@ struct NCShareDetailsView: View { Section(header: Text(NSLocalizedString("_governance_", comment: "")).font(.headline) ) { - labelPicker( + labelSelectorRow( title: "_sensitivity_label_", labels: data.availableSensitivityLabels, - selection: $model.selectedSensitivityLabelID - ) { oldValue, newValue in - await model.applySensitivityLabel(from: oldValue, to: newValue) + selectedIDs: sensitivitySelection + ) { + activeSelector = .sensitivity } labelSelectorRow( @@ -58,17 +67,7 @@ struct NCShareDetailsView: View { } .tint(Color(NCBrandColor.shared.getElement(account: model.account))) .sheet(item: $activeSelector) { selector in - let isRetention = selector == .retention - - NCGovernanceLabelSelectorView( - title: isRetention ? "_file_retention_" : "_legal_hold_", - labels: isRetention ? data.availableRetentionLabels : data.availableHoldLabels, - account: model.account, - initialSelection: isRetention ? model.selectedRetentionLabelIDs : model.selectedHoldLabelIDs - ) { newSelection in - await model.saveLabels(type: isRetention ? .retention : .hold, selectedIDs: newSelection) - } - .presentationDetents([.medium, .large]) + selectorSheet(selector, data: data) } case .error(let error): @@ -80,26 +79,46 @@ struct NCShareDetailsView: View { } } - private func labelPicker( - title: String, - labels: [NKGovernanceLabel], - selection: Binding, - onChange: @escaping (_ oldValue: String, _ newValue: String) async -> Void - ) -> some View { - HStack { - Text(NSLocalizedString(title, comment: "")) - .cappedFont(.body, maxDynamicType: .accessibility2) - Spacer() - Picker("", selection: selection) { - Text(NSLocalizedString("_none_", comment: "")).tag("") - ForEach(labels, id: \.id) { label in - Text(label.name).tag(label.id) - } - } - .pickerStyle(.menu) + private var sensitivitySelection: Set { + model.selectedSensitivityLabelID.isEmpty ? [] : [model.selectedSensitivityLabelID] + } + + private func selectorSheet(_ selector: GovernanceSelector, data: GovernanceData) -> some View { + let labels: [NKGovernanceLabel] + let initialSelection: Set + + switch selector { + case .sensitivity: + labels = data.availableSensitivityLabels + initialSelection = sensitivitySelection + case .retention: + labels = data.availableRetentionLabels + initialSelection = model.selectedRetentionLabelIDs + case .hold: + labels = data.availableHoldLabels + initialSelection = model.selectedHoldLabelIDs } - .onChange(of: selection.wrappedValue) { oldValue, newValue in - Task { await onChange(oldValue, newValue) } + + return NCGovernanceLabelSelectorView( + title: selector.title, + labels: labels, + account: model.account, + allowsMultipleSelection: selector != .sensitivity, + initialSelection: initialSelection + ) { newSelection in + await save(selector, newSelection: newSelection) + } + .presentationDetents([.medium, .large]) + } + + private func save(_ selector: GovernanceSelector, newSelection: Set) async { + switch selector { + case .sensitivity: + await model.applySensitivityLabel(from: model.selectedSensitivityLabelID, to: newSelection.first ?? "") + case .retention: + await model.saveLabels(type: .retention, selectedIDs: newSelection) + case .hold: + await model.saveLabels(type: .hold, selectedIDs: newSelection) } } From 2bc6871e686bb2b7ced85222cf5b5e07ae580549 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 16 Jul 2026 11:07:18 +0200 Subject: [PATCH 13/22] WIP Signed-off-by: Milen Pivchev --- .../Share/NCShareDetailsGovernanceModel.swift | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/iOSClient/Share/NCShareDetailsGovernanceModel.swift b/iOSClient/Share/NCShareDetailsGovernanceModel.swift index fd335cf0f5..2b73b08409 100644 --- a/iOSClient/Share/NCShareDetailsGovernanceModel.swift +++ b/iOSClient/Share/NCShareDetailsGovernanceModel.swift @@ -37,30 +37,21 @@ final class NCShareDetailsGovernanceModel { private var entityID: String { metadata.fileId } func load() async { - async let sensitivity = NextcloudKit.shared.getGovernanceAvailableSensitivityLabels(entityId: entityID, account: account) - async let retention = NextcloudKit.shared.getGovernanceAvailableRetentionLabels(entityId: entityID, account: account) - async let hold = NextcloudKit.shared.getGovernanceAvailableHoldLabels(entityId: entityID, account: account) - async let entity = NextcloudKit.shared.getGovernanceLabels(entityId: entityID, account: account) - - let entityResult = await entity - let sensitivityLabels = await sensitivity.labels - let retentionLabels = await retention.labels - let holdLabels = await hold.labels - - if entityResult.labels == nil, sensitivityLabels == nil, retentionLabels == nil, holdLabels == nil { - state = .error(entityResult.error) + let result = await NextcloudKit.shared.getGovernanceAvailableLabels(entityId: entityID, account: account) + + guard let labels = result.labels else { + state = .error(result.error) return } - let entityLabels = entityResult.labels - selectedSensitivityLabelID = entityLabels?.sensitivity?.id ?? "" - selectedRetentionLabelIDs = Set(entityLabels?.retention.map(\.id) ?? []) - selectedHoldLabelIDs = Set(entityLabels?.hold.map(\.id) ?? []) + selectedSensitivityLabelID = labels.sensitivity.first(where: \.isAssigned)?.id ?? "" + selectedRetentionLabelIDs = Set(labels.retention.filter(\.isAssigned).map(\.id)) + selectedHoldLabelIDs = Set(labels.hold.filter(\.isAssigned).map(\.id)) state = .loaded(GovernanceData( - availableSensitivityLabels: sensitivityLabels ?? [], - availableRetentionLabels: retentionLabels ?? [], - availableHoldLabels: holdLabels ?? [] + availableSensitivityLabels: labels.sensitivity, + availableRetentionLabels: labels.retention, + availableHoldLabels: labels.hold )) } From 764b161a3c17d84c29d995ddeb7956aa5c826421 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 16 Jul 2026 11:12:42 +0200 Subject: [PATCH 14/22] WIP Signed-off-by: Milen Pivchev --- iOSClient/Share/NCShareDetailsView.swift | 31 ++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/iOSClient/Share/NCShareDetailsView.swift b/iOSClient/Share/NCShareDetailsView.swift index 2466c134fe..97883282b8 100644 --- a/iOSClient/Share/NCShareDetailsView.swift +++ b/iOSClient/Share/NCShareDetailsView.swift @@ -48,7 +48,7 @@ struct NCShareDetailsView: View { activeSelector = .sensitivity } - labelSelectorRow( + labelSelectorSubtitleRow( title: "_file_retention_", labels: data.availableRetentionLabels, selectedIDs: model.selectedRetentionLabelIDs @@ -56,7 +56,7 @@ struct NCShareDetailsView: View { activeSelector = .retention } - labelSelectorRow( + labelSelectorSubtitleRow( title: "_legal_hold_", labels: data.availableHoldLabels, selectedIDs: model.selectedHoldLabelIDs @@ -147,6 +147,33 @@ struct NCShareDetailsView: View { } } + private func labelSelectorSubtitleRow( + title: String, + labels: [NKGovernanceLabel], + selectedIDs: Set, + action: @escaping () -> Void + ) -> some View { + Button(action: action) { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(NSLocalizedString(title, comment: "")) + .cappedFont(.body, maxDynamicType: .accessibility2) + .foregroundStyle(.primary) + + Text(summary(labels: labels, selectedIDs: selectedIDs)) + .font(.footnote) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: "chevron.right") + .font(.footnote.weight(.semibold)) + .foregroundStyle(.tertiary) + } + } + } + private func summary(labels: [NKGovernanceLabel], selectedIDs: Set) -> String { guard !selectedIDs.isEmpty else { return NSLocalizedString("_none_", comment: "") From e2a613b594fdc4463252566bfcd5e65f050c07de Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 16 Jul 2026 11:28:41 +0200 Subject: [PATCH 15/22] WIP Signed-off-by: Milen Pivchev --- .../Share/NCGovernanceLabelSelectorView.swift | 10 +++--- iOSClient/Share/NCShareDetailsView.swift | 36 +++++++++++++++---- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/iOSClient/Share/NCGovernanceLabelSelectorView.swift b/iOSClient/Share/NCGovernanceLabelSelectorView.swift index a7023db93e..921f81832f 100644 --- a/iOSClient/Share/NCGovernanceLabelSelectorView.swift +++ b/iOSClient/Share/NCGovernanceLabelSelectorView.swift @@ -47,7 +47,7 @@ struct NCGovernanceLabelSelectorView: View { Button { toggle(label.id) } label: { - row(dot: color(for: label), name: label.name, isSelected: selected.contains(label.id)) + row(dot: label.displayColor, name: label.name, isSelected: selected.contains(label.id)) } } } @@ -96,7 +96,7 @@ struct NCGovernanceLabelSelectorView: View { .frame(width: 10, height: 10) Text(name) - .foregroundStyle(.primary) + .foregroundStyle(Color.primary) Spacer() @@ -119,9 +119,11 @@ struct NCGovernanceLabelSelectorView: View { selected.insert(id) } } +} - private func color(for label: NKGovernanceLabel) -> Color { - if let color = UIColor(hex: label.color) { +extension NKGovernanceLabel { + var displayColor: Color { + if let color = UIColor(hex: color) { return Color(color) } diff --git a/iOSClient/Share/NCShareDetailsView.swift b/iOSClient/Share/NCShareDetailsView.swift index 97883282b8..72c230a086 100644 --- a/iOSClient/Share/NCShareDetailsView.swift +++ b/iOSClient/Share/NCShareDetailsView.swift @@ -132,17 +132,17 @@ struct NCShareDetailsView: View { HStack { Text(NSLocalizedString(title, comment: "")) .cappedFont(.body, maxDynamicType: .accessibility2) - .foregroundStyle(.primary) + .foregroundStyle(Color.primary) Spacer() Text(summary(labels: labels, selectedIDs: selectedIDs)) - .foregroundStyle(.secondary) + .foregroundStyle(Color.secondary) .lineLimit(1) Image(systemName: "chevron.right") .font(.footnote.weight(.semibold)) - .foregroundStyle(.tertiary) + .foregroundStyle(Color(.tertiaryLabel)) } } } @@ -158,22 +158,44 @@ struct NCShareDetailsView: View { VStack(alignment: .leading, spacing: 2) { Text(NSLocalizedString(title, comment: "")) .cappedFont(.body, maxDynamicType: .accessibility2) - .foregroundStyle(.primary) + .foregroundStyle(Color.primary) - Text(summary(labels: labels, selectedIDs: selectedIDs)) + subtitle(labels: labels, selectedIDs: selectedIDs) .font(.footnote) - .foregroundStyle(.secondary) + .foregroundStyle(Color.secondary) } Spacer() Image(systemName: "chevron.right") .font(.footnote.weight(.semibold)) - .foregroundStyle(.tertiary) + .foregroundStyle(Color(.tertiaryLabel)) } } } + private func subtitle(labels: [NKGovernanceLabel], selectedIDs: Set) -> Text { + let selected = labels.filter { selectedIDs.contains($0.id) } + + guard !selected.isEmpty else { + return Text(NSLocalizedString("_none_", comment: "")) + } + + var text = Text("") + + for (index, label) in selected.enumerated() { + if index > 0 { + text = text + Text(", ") + } + + text = text + + Text(Image(systemName: "circle.fill")).font(.caption2).foregroundStyle(label.displayColor) + + Text(" \(label.name)") + } + + return text + } + private func summary(labels: [NKGovernanceLabel], selectedIDs: Set) -> String { guard !selectedIDs.isEmpty else { return NSLocalizedString("_none_", comment: "") From 095e87fc86dc10142f5fc656fd6a29515303e283 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Fri, 17 Jul 2026 15:04:04 +0200 Subject: [PATCH 16/22] WIP Signed-off-by: Milen Pivchev --- .../Data/NCManageDatabase+Metadata.swift | 4 +- iOSClient/Share/NCMetadataPermissions.swift | 6 +++ iOSClient/Share/NCShareDetailsView.swift | 37 +------------------ 3 files changed, 9 insertions(+), 38 deletions(-) diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 013d0c2d03..4961deee52 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -189,7 +189,7 @@ extension tableMetadata { } var isCopyableMovable: Bool { - !isDirectoryE2EE && !e2eEncrypted + !isDirectoryE2EE && !e2eEncrypted || NCMetadataPermissions.canMoveAndDelete(self) } var isModifiableWithQuickLook: Bool { @@ -217,7 +217,7 @@ extension tableMetadata { } var isDeletable: Bool { - if (!isDirectoryE2EE && e2eEncrypted) || !NCMetadataPermissions.canDelete(self) { + if (!isDirectoryE2EE && e2eEncrypted) || !NCMetadataPermissions.canDelete(self) || !NCMetadataPermissions.canMoveAndDelete(self) { return false } return true diff --git a/iOSClient/Share/NCMetadataPermissions.swift b/iOSClient/Share/NCMetadataPermissions.swift index caacb3d2d1..dac4313c48 100644 --- a/iOSClient/Share/NCMetadataPermissions.swift +++ b/iOSClient/Share/NCMetadataPermissions.swift @@ -26,10 +26,16 @@ enum NCMetadataPermissions { return metadata.permissions.contains(permissionCanCreateFolder) } + /** Note: If a folder is shared it will be unshared instead of deleted */ static func canDelete(_ metadata: tableMetadata) -> Bool { return metadata.permissions.contains(permissionCanDeleteOrUnshare) } + /// This includes deleting as deleting a file is moving it to the trash + static func canMoveAndDelete(_ metadata: tableMetadata) -> Bool { + return metadata.permissions.contains(permissionCanMove) + } + static func canRename(_ metadata: tableMetadata) -> Bool { return metadata.permissions.contains(permissionCanRename) } diff --git a/iOSClient/Share/NCShareDetailsView.swift b/iOSClient/Share/NCShareDetailsView.swift index 72c230a086..9cc48ee90f 100644 --- a/iOSClient/Share/NCShareDetailsView.swift +++ b/iOSClient/Share/NCShareDetailsView.swift @@ -40,7 +40,7 @@ struct NCShareDetailsView: View { Section(header: Text(NSLocalizedString("_governance_", comment: "")).font(.headline) ) { - labelSelectorRow( + labelSelectorSubtitleRow( title: "_sensitivity_label_", labels: data.availableSensitivityLabels, selectedIDs: sensitivitySelection @@ -122,31 +122,6 @@ struct NCShareDetailsView: View { } } - private func labelSelectorRow( - title: String, - labels: [NKGovernanceLabel], - selectedIDs: Set, - action: @escaping () -> Void - ) -> some View { - Button(action: action) { - HStack { - Text(NSLocalizedString(title, comment: "")) - .cappedFont(.body, maxDynamicType: .accessibility2) - .foregroundStyle(Color.primary) - - Spacer() - - Text(summary(labels: labels, selectedIDs: selectedIDs)) - .foregroundStyle(Color.secondary) - .lineLimit(1) - - Image(systemName: "chevron.right") - .font(.footnote.weight(.semibold)) - .foregroundStyle(Color(.tertiaryLabel)) - } - } - } - private func labelSelectorSubtitleRow( title: String, labels: [NKGovernanceLabel], @@ -196,16 +171,6 @@ struct NCShareDetailsView: View { return text } - private func summary(labels: [NKGovernanceLabel], selectedIDs: Set) -> String { - guard !selectedIDs.isEmpty else { - return NSLocalizedString("_none_", comment: "") - } - - return labels - .filter { selectedIDs.contains($0.id) } - .map(\.name) - .joined(separator: ", ") - } } final class NCShareDetailsViewController: UIHostingController, NCSharePagingContent { From 0df6dfb79129d94ca4b0d039470f90ecd2478fe0 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 21 Jul 2026 15:53:08 +0200 Subject: [PATCH 17/22] WIP Signed-off-by: Milen Pivchev --- iOSClient/Share/NCShareHeader.swift | 2 +- iOSClient/Share/NCShareHeader.xib | 7 ++----- iOSClient/Share/NCSharePaging.swift | 7 ++++++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/iOSClient/Share/NCShareHeader.swift b/iOSClient/Share/NCShareHeader.swift index e0a611930c..c7670551cd 100644 --- a/iOSClient/Share/NCShareHeader.swift +++ b/iOSClient/Share/NCShareHeader.swift @@ -70,7 +70,7 @@ class NCShareHeader: UIView { info.text = utilityFileSystem.transformedSize(metadata.size) + ", " + NCUtility().getRelativeDateTitle(metadata.date as Date) tagListView.marginY = 8 - tagListView.backgroundColor = .secondarySystemBackground + tagListView.backgroundColor = .clear refreshTags(metadata.tagNames, tagModels: metadata.tags.map(\.nkTag)) setNeedsLayout() diff --git a/iOSClient/Share/NCShareHeader.xib b/iOSClient/Share/NCShareHeader.xib index 50574f7299..0145ca4094 100644 --- a/iOSClient/Share/NCShareHeader.xib +++ b/iOSClient/Share/NCShareHeader.xib @@ -47,12 +47,9 @@ - + - - - - + diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index 730f47ea5c..ac3874f3ff 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -298,5 +298,10 @@ private struct NCViewControllerRepresentable: UIViewControllerRepresentable { let viewController: UIViewController func makeUIViewController(context: Context) -> UIViewController { viewController } - func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { + + // TabView(.page) does not propagate appearance trait changes to represented VCs (as of iOS 18.4), seems a SwiftUI bug... + uiViewController.view.overrideUserInterfaceStyle = context.environment.colorScheme == .dark ? .dark : .light + } } From fb8303da6ab1d9b835948d39a50030dba74b8d89 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Tue, 21 Jul 2026 17:20:15 +0200 Subject: [PATCH 18/22] WIP Signed-off-by: Milen Pivchev --- iOSClient/Data/NCManageDatabase+Metadata.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOSClient/Data/NCManageDatabase+Metadata.swift b/iOSClient/Data/NCManageDatabase+Metadata.swift index 4961deee52..98e4c0cd65 100644 --- a/iOSClient/Data/NCManageDatabase+Metadata.swift +++ b/iOSClient/Data/NCManageDatabase+Metadata.swift @@ -189,7 +189,7 @@ extension tableMetadata { } var isCopyableMovable: Bool { - !isDirectoryE2EE && !e2eEncrypted || NCMetadataPermissions.canMoveAndDelete(self) + !isDirectoryE2EE && !e2eEncrypted && NCMetadataPermissions.canMoveAndDelete(self) } var isModifiableWithQuickLook: Bool { From 09223806b14b930e6fc5bb837fcfccf06101aa22 Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 22 Jul 2026 16:09:01 +0200 Subject: [PATCH 19/22] refactor Signed-off-by: Milen Pivchev --- iOSClient/Share/NCGovernanceLabelSelectorView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOSClient/Share/NCGovernanceLabelSelectorView.swift b/iOSClient/Share/NCGovernanceLabelSelectorView.swift index 921f81832f..6cde61f00a 100644 --- a/iOSClient/Share/NCGovernanceLabelSelectorView.swift +++ b/iOSClient/Share/NCGovernanceLabelSelectorView.swift @@ -61,7 +61,7 @@ struct NCGovernanceLabelSelectorView: View { } ToolbarItem(placement: .confirmationAction) { - Button(NSLocalizedString("_save_", comment: ""), systemImage: "checkmark.circle.fill") { + Button(NSLocalizedString("_save_", comment: ""), systemImage: "checkmark") { Task { isSaving = true await onSave(selected) From ea9a4b433f062baead2829bf7cb7db956ea1914d Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 22 Jul 2026 16:58:19 +0200 Subject: [PATCH 20/22] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 26 +++++++++++++------ .../Share/NCShareDetailsGovernanceModel.swift | 5 ++-- iOSClient/Share/NCShareDetailsView.swift | 6 ++--- iOSClient/Share/NCSharePaging.swift | 1 + .../en.lproj/Localizable.strings | 2 ++ 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index 58838dcd2e..e235a671a7 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -4114,7 +4114,7 @@ F76DA967277B77E90082465B /* XCRemoteSwiftPackageReference "DropDown" */, F710FC78277B7CFF00AA9FBF /* XCRemoteSwiftPackageReference "realm-swift" */, F753BA91281FD8010015BFB6 /* XCRemoteSwiftPackageReference "EasyTipView" */, - F783034028B511D200B84583 /* XCLocalSwiftPackageReference "../NextcloudKit-nc-governance" */, + F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */, F734B06428E75C0100E180D5 /* XCRemoteSwiftPackageReference "TLPhotoPicker" */, F77333862927A72100466E35 /* XCRemoteSwiftPackageReference "OpenSSL" */, F77BC3E9293E5268005F2B08 /* XCRemoteSwiftPackageReference "swifter" */, @@ -6503,13 +6503,6 @@ }; /* End XCConfigurationList section */ -/* Begin XCLocalSwiftPackageReference section */ - F783034028B511D200B84583 /* XCLocalSwiftPackageReference "../NextcloudKit-nc-governance" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = "../NextcloudKit-nc-governance"; - }; -/* End XCLocalSwiftPackageReference section */ - /* Begin XCRemoteSwiftPackageReference section */ F3374AEE2D78B01B002A38F9 /* XCRemoteSwiftPackageReference "swift-collections" */ = { isa = XCRemoteSwiftPackageReference; @@ -6623,6 +6616,14 @@ kind = branch; }; }; + F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/nextcloud/NextcloudKit"; + requirement = { + branch = "nc-governance"; + kind = branch; + }; + }; F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/bmoliveira/MarkdownKit"; @@ -6766,6 +6767,7 @@ }; F37208B72BAB63EF006B5430 /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F37208B92BAB63EF006B5430 /* TLPhotoPicker */ = { @@ -6880,18 +6882,22 @@ }; F72AD70C28C24B93006CB92D /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F72AD70E28C24BA1006CB92D /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F72AD71028C24BBB006CB92D /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F72AD71228C24BCC006CB92D /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F72D7EB6263B1207000B3DFC /* MarkdownKit */ = { @@ -6916,6 +6922,7 @@ }; F7490E8A29882CE4009DCE94 /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F753BA92281FD8020015BFB6 /* EasyTipView */ = { @@ -6980,6 +6987,7 @@ }; F783034328B5142B00B84583 /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F788ECC6263AAAFA00ADC67F /* MarkdownKit */ = { @@ -6994,6 +7002,7 @@ }; F7A8D72328F1771B008BBE1C /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F7A8D72728F17728008BBE1C /* RealmSwift */ = { @@ -7003,6 +7012,7 @@ }; F7C55C9E2FB5B83A004A974F /* NextcloudKit */ = { isa = XCSwiftPackageProductDependency; + package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; productName = NextcloudKit; }; F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */ = { diff --git a/iOSClient/Share/NCShareDetailsGovernanceModel.swift b/iOSClient/Share/NCShareDetailsGovernanceModel.swift index 2b73b08409..e2103b3a71 100644 --- a/iOSClient/Share/NCShareDetailsGovernanceModel.swift +++ b/iOSClient/Share/NCShareDetailsGovernanceModel.swift @@ -14,7 +14,7 @@ struct GovernanceData { enum GovernanceViewState { case loading case loaded(GovernanceData) - case error(Error) + case error } @MainActor @@ -40,7 +40,8 @@ final class NCShareDetailsGovernanceModel { let result = await NextcloudKit.shared.getGovernanceAvailableLabels(entityId: entityID, account: account) guard let labels = result.labels else { - state = .error(result.error) + nkLog(error: "Could not load governance labels: \(result.error.errorDescription) (\(result.error.errorCode))") + state = .error return } diff --git a/iOSClient/Share/NCShareDetailsView.swift b/iOSClient/Share/NCShareDetailsView.swift index 9cc48ee90f..ca62a4eda2 100644 --- a/iOSClient/Share/NCShareDetailsView.swift +++ b/iOSClient/Share/NCShareDetailsView.swift @@ -34,7 +34,7 @@ struct NCShareDetailsView: View { ZStack { switch model.state { case .loading: - Text("Loading") + ProgressView() case .loaded(let data): Form { Section(header: @@ -70,8 +70,8 @@ struct NCShareDetailsView: View { selectorSheet(selector, data: data) } - case .error(let error): - Text(error.localizedDescription) + case .error: + Text(NSLocalizedString("_governance_labels_load_error_", comment: "")) } } .task { diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index ac3874f3ff..5da74b406b 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -276,6 +276,7 @@ struct NCSharePagingContentView: View { } } .pickerStyle(.segmented) + .accessibilityLabel(NSLocalizedString("_sections_", comment: "")) .padding(.horizontal, 16) .padding(.vertical, 8) .tint(tint) diff --git a/iOSClient/Supporting Files/en.lproj/Localizable.strings b/iOSClient/Supporting Files/en.lproj/Localizable.strings index cadfb30fea..31aeff1f27 100644 --- a/iOSClient/Supporting Files/en.lproj/Localizable.strings +++ b/iOSClient/Supporting Files/en.lproj/Localizable.strings @@ -862,3 +862,5 @@ You can stop it at any time, adjust the settings, and enable it again."; "_file_retention_" = "File retention"; "_legal_hold_" = "Legal hold"; "_none_" = "None"; +"_sections_" = "Sections"; +"_governance_labels_load_error_" = "Governance labels could not be loaded."; From 9cef43cdd7659cc6d7f752ceeac14208c356318b Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Wed, 22 Jul 2026 17:07:51 +0200 Subject: [PATCH 21/22] WIP Signed-off-by: Milen Pivchev --- Nextcloud.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index e235a671a7..653144ea86 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -6620,7 +6620,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nextcloud/NextcloudKit"; requirement = { - branch = "nc-governance"; + branch = main; kind = branch; }; }; From 737a1a2824be26f696843cb39f42523f2f2b33cb Mon Sep 17 00:00:00 2001 From: Milen Pivchev Date: Thu, 23 Jul 2026 11:43:07 +0200 Subject: [PATCH 22/22] PR feedback Signed-off-by: Milen Pivchev --- iOSClient/Share/NCGovernanceLabelSelectorView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iOSClient/Share/NCGovernanceLabelSelectorView.swift b/iOSClient/Share/NCGovernanceLabelSelectorView.swift index 6cde61f00a..5b4637fae8 100644 --- a/iOSClient/Share/NCGovernanceLabelSelectorView.swift +++ b/iOSClient/Share/NCGovernanceLabelSelectorView.swift @@ -58,6 +58,7 @@ struct NCGovernanceLabelSelectorView: View { Button(NSLocalizedString("_cancel_", comment: ""), systemImage: "xmark") { dismiss() } + .tint(Color.primary) } ToolbarItem(placement: .confirmationAction) { @@ -69,6 +70,7 @@ struct NCGovernanceLabelSelectorView: View { dismiss() } } + .buttonStyle(.borderedProminent) } } .overlay {