Merge branch 'main' into bsc-662-catch-up

This commit is contained in:
Bryan Culver
2022-11-21 23:07:29 -05:00
113 changed files with 2473 additions and 1485 deletions

View File

@@ -128,14 +128,17 @@ class MasterFeedTableViewCell : VibrantTableViewCell {
self.disclosureButton?.accessibilityLabel = NSLocalizedString("Collapse Folder", comment: "Collapse Folder")
self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 1.570796)
} else {
self.disclosureButton?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder")
self.disclosureButton?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder")
self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 0)
}
}
}
override func applyThemeProperties() {
super.applyThemeProperties()
override func updateConfiguration(using state: UICellConfigurationState) {
backgroundConfiguration = UIBackgroundConfiguration.listSidebarCell().updated(for: state)
if state.isSelected {
backgroundConfiguration?.backgroundColor = AppAssets.secondaryAccentColor
}
}
override func willTransition(to state: UITableViewCell.StateMask) {
@@ -171,8 +174,10 @@ class MasterFeedTableViewCell : VibrantTableViewCell {
let iconTintColor: UIColor
if isHighlighted || isSelected {
disclosureButton?.tintColor = AppAssets.vibrantTextColor
iconTintColor = AppAssets.vibrantTextColor
} else {
disclosureButton?.tintColor = AppAssets.secondaryAccentColor
if let preferredColor = iconImage?.preferredColor {
iconTintColor = UIColor(cgColor: preferredColor)
} else {

View File

@@ -18,33 +18,63 @@ extension MasterFeedViewController: UITableViewDropDelegate {
}
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
guard let destIndexPath = destinationIndexPath, destIndexPath.section > 0, tableView.hasActiveDrag else {
guard tableView.hasActiveDrag else {
return UITableViewDropProposal(operation: .forbidden)
}
guard let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
let sourceWebFeed = sourceNode.representedObject as? WebFeed else {
return UITableViewDropProposal(operation: .forbidden)
}
guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? Feed,
let destAccount = destFeed.account,
let destCell = tableView.cellForRow(at: destIndexPath) else {
return UITableViewDropProposal(operation: .forbidden)
}
var successOperation = UIDropOperation.move
if let destinationIndexPath = destinationIndexPath,
let sourceIndexPath = coordinator.indexPathFor(sourceNode),
destinationIndexPath.section != sourceIndexPath.section {
successOperation = .copy
}
guard let correctedIndexPath = correctDestinationIndexPath(session: session) else {
// We didn't hit the corrected indexPath, but this at least it gets the section right
guard let section = destinationIndexPath?.section,
let account = coordinator.nodeFor(section)?.representedObject as? Account,
!account.hasChildWebFeed(withURL: sourceWebFeed.url) else {
return UITableViewDropProposal(operation: .forbidden)
}
return UITableViewDropProposal(operation: successOperation, intent: .insertAtDestinationIndexPath)
}
guard correctedIndexPath.section > 0 else {
return UITableViewDropProposal(operation: .forbidden)
}
guard let correctDestNode = coordinator.nodeFor(correctedIndexPath),
let correctDestFeed = correctDestNode.representedObject as? Feed,
let correctDestAccount = correctDestFeed.account else {
return UITableViewDropProposal(operation: .forbidden)
}
// Validate account specific behaviors...
if destAccount.behaviors.contains(.disallowFeedInMultipleFolders),
let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
let sourceWebFeed = sourceNode.representedObject as? WebFeed,
sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasWebFeed(withURL: sourceWebFeed.url) {
if correctDestAccount.behaviors.contains(.disallowFeedInMultipleFolders),
sourceWebFeed.account?.accountID != correctDestAccount.accountID && correctDestAccount.hasWebFeed(withURL: sourceWebFeed.url) {
return UITableViewDropProposal(operation: .forbidden)
}
// Determine the correct drop proposal
if destFeed is Folder {
if session.location(in: destCell).y >= 0 {
return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath)
if let correctFolder = correctDestFeed as? Folder {
if correctFolder.hasChildWebFeed(withURL: sourceWebFeed.url) {
return UITableViewDropProposal(operation: .forbidden)
} else {
return UITableViewDropProposal(operation: .move, intent: .unspecified)
return UITableViewDropProposal(operation: successOperation, intent: .insertIntoDestinationIndexPath)
}
} else {
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
if let parentContainer = correctDestNode.parent?.representedObject as? Container, !parentContainer.hasChildWebFeed(withURL: sourceWebFeed.url) {
return UITableViewDropProposal(operation: successOperation, intent: .insertAtDestinationIndexPath)
} else {
return UITableViewDropProposal(operation: .forbidden)
}
}
}
@@ -52,33 +82,23 @@ extension MasterFeedViewController: UITableViewDropDelegate {
func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) {
guard let dragItem = dropCoordinator.items.first?.dragItem,
let dragNode = dragItem.localObject as? Node,
let source = dragNode.parent?.representedObject as? Container,
let destIndexPath = dropCoordinator.destinationIndexPath else {
return
}
let isFolderDrop: Bool = {
if coordinator.nodeFor(destIndexPath)?.representedObject is Folder, let propCell = tableView.cellForRow(at: destIndexPath) {
return dropCoordinator.session.location(in: propCell).y >= 0
}
return false
}()
let source = dragNode.parent?.representedObject as? Container else {
return
}
// Based on the drop we have to determine a node to start looking for a parent container.
let destNode: Node? = {
guard let destIndexPath = correctDestinationIndexPath(session: dropCoordinator.session) else { return nil }
if isFolderDrop {
return coordinator.nodeFor(destIndexPath)
} else {
if destIndexPath.row == 0 {
return coordinator.nodeFor(IndexPath(row: 0, section: destIndexPath.section))
} else if destIndexPath.row > 0 {
return coordinator.nodeFor(IndexPath(row: destIndexPath.row - 1, section: destIndexPath.section))
if coordinator.nodeFor(destIndexPath)?.representedObject is Folder {
if dropCoordinator.proposal.intent == .insertAtDestinationIndexPath {
return coordinator.nodeFor(destIndexPath.section)
} else {
return nil
return coordinator.nodeFor(destIndexPath)
}
} else {
return nil
}
}()
// Now we start looking for the parent container
@@ -86,8 +106,11 @@ extension MasterFeedViewController: UITableViewDropDelegate {
if let container = (destNode?.representedObject as? Container) ?? (destNode?.parent?.representedObject as? Container) {
return container
} else {
// We didn't hit the corrected indexPath, but this at least gets the section right
guard let section = dropCoordinator.destinationIndexPath?.section else { return nil }
// If we got here, we are trying to drop on an empty section header. Go and find the Account for this section
return coordinator.rootNode.childAtIndex(destIndexPath.section)?.representedObject as? Account
return coordinator.nodeFor(section)?.representedObject as? Account
}
}()
@@ -96,10 +119,25 @@ extension MasterFeedViewController: UITableViewDropDelegate {
if source.account == destination.account {
moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination)
} else {
moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination)
copyWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination)
}
}
}
private extension MasterFeedViewController {
func correctDestinationIndexPath(session: UIDropSession) -> IndexPath? {
let location = session.location(in: tableView)
var correctDestination: IndexPath?
tableView.performUsingPresentationValues {
correctDestination = tableView.indexPathForRow(at: location)
}
return correctDestination
}
func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
guard sourceContainer !== destinationContainer else { return }
@@ -115,7 +153,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
}
}
func moveWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
func copyWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) {
@@ -123,15 +161,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
destinationContainer.account?.addWebFeed(existingFeed, to: destinationContainer) { result in
switch result {
case .success:
sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
BatchUpdate.shared.end()
switch result {
case .success:
break
case .failure(let error):
self.presentError(error)
}
}
BatchUpdate.shared.end()
case .failure(let error):
BatchUpdate.shared.end()
self.presentError(error)
@@ -144,15 +174,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
destinationContainer.account?.createWebFeed(url: feed.url, name: feed.editedName, container: destinationContainer, validateFeed: false) { result in
switch result {
case .success:
sourceContainer.account?.removeWebFeed(feed, from: sourceContainer) { result in
BatchUpdate.shared.end()
switch result {
case .success:
break
case .failure(let error):
self.presentError(error)
}
}
BatchUpdate.shared.end()
case .failure(let error):
BatchUpdate.shared.end()
self.presentError(error)
@@ -164,3 +186,11 @@ extension MasterFeedViewController: UITableViewDropDelegate {
}
private extension Container {
func hasChildWebFeed(withURL url: String) -> Bool {
return topLevelWebFeeds.contains(where: { $0.url == url })
}
}

View File

@@ -7,6 +7,7 @@
//
import UIKit
import SwiftUI
import Account
import Articles
import RSCore
@@ -16,13 +17,15 @@ import SafariServices
class MasterFeedViewController: UITableViewController, UndoableCommandRunner, MainControllerIdentifiable {
@IBOutlet weak var filterButton: UIBarButtonItem!
private var refreshProgressView: RefreshProgressView?
@IBOutlet weak var addNewItemButton: UIBarButtonItem! {
didSet {
addNewItemButton.primaryAction = nil
}
}
let refreshProgressModel = RefreshProgressModel()
lazy var progressBarViewController = UIHostingController(rootView: RefreshProgressView(progressBarMode: refreshProgressModel))
var mainControllerIdentifer = MainControllerIdentifier.masterFeed
weak var coordinator: SceneCoordinator!
@@ -71,11 +74,17 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(configureContextMenu(_:)), name: .ActiveExtensionPointsDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
refreshControl = UIRefreshControl()
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
refreshControl!.tintColor = .clear
progressBarViewController.view.backgroundColor = .clear
progressBarViewController.view.translatesAutoresizingMaskIntoConstraints = false
let refreshProgressItemButton = UIBarButtonItem(customView: progressBarViewController.view)
toolbarItems?.insert(refreshProgressItemButton, at: 2)
configureToolbar()
becomeFirstResponder()
}
@@ -139,6 +148,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
}
}
@objc func displayNameDidChange(_ note: Notification) {
guard let object = note.object as? AnyObject else {
return
}
reloadCell(for: object)
}
@objc func contentSizeCategoryDidChange(_ note: Notification) {
resetEstimatedRowHeight()
tableView.reloadData()
@@ -515,7 +531,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
func updateFeedSelection(animations: Animations) {
if let indexPath = coordinator.currentFeedIndexPath {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
if indexPath != tableView.indexPathForSelectedRow {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
}
} else {
if let indexPath = tableView.indexPathForSelectedRow {
if animations.contains(.select) {
@@ -587,7 +605,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner, Ma
} else {
setFilterButtonToInactive()
}
refreshProgressView?.update()
refreshProgressModel.update()
addNewItemButton?.isEnabled = !AccountManager.shared.activeAccounts.isEmpty
configureContextMenu()
@@ -720,16 +738,6 @@ extension MasterFeedViewController: MasterFeedTableViewCellDelegate {
private extension MasterFeedViewController {
func configureToolbar() {
guard let refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView else {
return
}
self.refreshProgressView = refreshProgressView
let refreshProgressItemButton = UIBarButtonItem(customView: refreshProgressView)
toolbarItems?.insert(refreshProgressItemButton, at: 2)
}
func setFilterButtonToActive() {
filterButton?.image = AppAssets.filterActiveImage
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Feeds", comment: "Selected - Filter Read Feeds")
@@ -831,6 +839,12 @@ private extension MasterFeedViewController {
completion(cell as! MasterFeedTableViewCell, indexPath)
}
}
private func reloadCell(for object: AnyObject) {
guard let indexPath = coordinator.indexPathFor(object) else { return }
tableView.reloadRows(at: [indexPath], with: .none)
restoreSelectionIfNecessary(adjustScroll: false)
}
private func reloadAllVisibleCells(completion: (() -> Void)? = nil) {
guard let indexPaths = tableView.indexPathsForVisibleRows else { return }
@@ -1050,8 +1064,7 @@ private extension MasterFeedViewController {
return nil
}
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String
let title = NSLocalizedString("Mark All as Read", comment: "Command")
let cancel = {
completion(true)
}
@@ -1131,8 +1144,7 @@ private extension MasterFeedViewController {
return nil
}
let localizedMenuText = NSLocalizedString("Mark All as Read", comment: "Command")
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
let title = NSLocalizedString("Mark All as Read", comment: "Command")
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
if let articles = try? feed.fetchUnreadArticles() {
@@ -1233,8 +1245,7 @@ private extension MasterFeedViewController {
return nil
}
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, account.nameForDisplay) as String
let title = NSLocalizedString("Mark All as Read", comment: "Command")
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
// If you don't have this delay the screen flashes when it executes this code

View File

@@ -1,27 +1,82 @@
//
// RefeshProgressView.swift
// NetNewsWire-iOS
// ProgressBarView.swift
// NetNewsWire
//
// Created by Maurice Parker on 10/24/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
// Created by Maurice Parker on 11/11/22.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
// IndetermineProgressView inspired by https://daringsnowball.net/articles/indeterminate-linear-progress-view/
import UIKit
import SwiftUI
import Account
class RefreshProgressView: UIView {
struct RefreshProgressView: View {
@IBOutlet weak var progressView: UIProgressView!
@IBOutlet weak var label: UILabel!
static let width: CGFloat = 100
static let height: CGFloat = 5
override func awakeFromNib() {
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil)
update()
scheduleUpdateRefreshLabel()
@ObservedObject var refreshProgressModel: RefreshProgressModel
@State private var offset: CGFloat = 0
isAccessibilityElement = true
accessibilityTraits = [.updatesFrequently, .notEnabled]
init(progressBarMode: RefreshProgressModel) {
self.refreshProgressModel = progressBarMode
}
var body: some View {
ZStack {
if refreshProgressModel.isRefreshing {
if refreshProgressModel.isIndeterminate {
indeterminateProgressView
} else {
ProgressView(value: refreshProgressModel.progress)
.progressViewStyle(LinearProgressViewStyle())
.frame(width: Self.width, height: Self.height)
}
} else {
Text(refreshProgressModel.label)
.accessibilityLabel(refreshProgressModel.label)
.font(.footnote)
.foregroundColor(.secondary)
}
}
.frame(width: 200, height: 44)
}
var indeterminateProgressView: some View {
Rectangle()
.foregroundColor(.gray.opacity(0.15))
.overlay(
Rectangle()
.foregroundColor(Color.accentColor)
.frame(width: Self.width * 0.26, height: Self.height)
.clipShape(Capsule())
.offset(x: -Self.width * 0.6, y: 0)
.offset(x: Self.width * 1.2 * self.offset, y: 0)
.animation(.default.repeatForever().speed(0.265), value: self.offset)
.onAppear {
withAnimation {
self.offset = 1
}
}
.onDisappear {
self.offset = 0
}
)
.clipShape(Capsule())
.frame(width: Self.width, height: Self.height)
}
}
class RefreshProgressModel: ObservableObject {
@Published var isRefreshing = false
@Published var isIndeterminate = false
@Published var progress = 0.0
@Published var label = String()
init() {
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
}
func update() {
@@ -31,52 +86,32 @@ class RefreshProgressView: UIView {
updateRefreshLabel()
}
}
override func didMoveToSuperview() {
progressChanged(animated: false)
}
@objc func progressDidChange(_ note: Notification) {
progressChanged(animated: true)
}
@objc func contentSizeCategoryDidChange(_ note: Notification) {
// This hack is probably necessary because custom views in the toolbar don't get
// notifications that the content size changed.
label.font = UIFont.preferredFont(forTextStyle: .footnote)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
// MARK: Private
private extension RefreshProgressView {
private extension RefreshProgressModel {
func progressChanged(animated: Bool) {
// Layout may crash if not in the view hierarchy.
// https://github.com/Ranchero-Software/NetNewsWire/issues/1764
let isInViewHierarchy = self.superview != nil
let progress = AccountManager.shared.combinedRefreshProgress
if progress.isComplete {
if isInViewHierarchy {
progressView.setProgress(1, animated: animated)
}
let combinedRefreshProgress = AccountManager.shared.combinedRefreshProgress
isIndeterminate = combinedRefreshProgress.isIndeterminate
if combinedRefreshProgress.isComplete {
isRefreshing = false
progress = 1
func completeLabel() {
// Check that there are no pending downloads.
if AccountManager.shared.combinedRefreshProgress.isComplete {
self.updateRefreshLabel()
self.label.isHidden = false
self.progressView.isHidden = true
if self.superview != nil {
self.progressView.setProgress(0, animated: animated)
}
updateRefreshLabel()
progress = 0
}
}
@@ -88,19 +123,16 @@ private extension RefreshProgressView {
completeLabel()
}
} else {
label.isHidden = true
progressView.isHidden = false
if isInViewHierarchy {
let percent = Float(progress.numberCompleted) / Float(progress.numberOfTasks)
isRefreshing = true
let percent = Double(combinedRefreshProgress.numberCompleted) / Double(combinedRefreshProgress.numberOfTasks)
// Don't let the progress bar go backwards unless we need to go back more than 25%
if percent > progressView.progress || progressView.progress - percent > 0.25 {
progressView.setProgress(percent, animated: animated)
}
// Don't let the progress bar go backwards unless we need to go back more than 25%
if percent > progress || (progress - percent) > 0.25 {
progress = percent
}
}
}
func updateRefreshLabel() {
if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime {
@@ -111,17 +143,15 @@ private extension RefreshProgressView {
let refreshed = relativeDateTimeFormatter.localizedString(for: accountLastArticleFetchEndTime, relativeTo: Date())
let localizedRefreshText = NSLocalizedString("Updated %@", comment: "Updated")
let refreshText = NSString.localizedStringWithFormat(localizedRefreshText as NSString, refreshed) as String
label.text = refreshText
label = refreshText
} else {
label.text = NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
label = NSLocalizedString("Updated Just Now", comment: "Updated Just Now")
}
} else {
label.text = ""
label = ""
}
accessibilityLabel = label.text
}
func scheduleUpdateRefreshLabel() {

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="ejl-zC-eNy" customClass="RefreshProgressView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="461" height="90"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progress="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="Ds3-59-ooT" customClass="RoundedProgressView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="180.5" y="42.5" width="100" height="5"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" constant="100" id="ReS-sT-7EN"/>
<constraint firstAttribute="height" constant="5" id="oDX-bb-24H"/>
</constraints>
</progressView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7mJ-VZ-zqU">
<rect key="frame" x="214" y="34" width="33" height="22"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="sNo-8i-tO3"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Ds3-59-ooT" firstAttribute="centerX" secondItem="ejl-zC-eNy" secondAttribute="centerX" id="5Rv-6l-HSL"/>
<constraint firstItem="Ds3-59-ooT" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ejl-zC-eNy" secondAttribute="leading" id="Bck-uf-0G7"/>
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="bottom" secondItem="sNo-8i-tO3" secondAttribute="bottom" id="DVn-hI-PhH"/>
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="sNo-8i-tO3" secondAttribute="leading" id="Sbp-yf-ts9"/>
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="centerY" secondItem="ejl-zC-eNy" secondAttribute="centerY" id="Shb-X2-Fwc"/>
<constraint firstItem="7mJ-VZ-zqU" firstAttribute="centerX" secondItem="ejl-zC-eNy" secondAttribute="centerX" id="lFg-fm-YmV"/>
<constraint firstItem="sNo-8i-tO3" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="7mJ-VZ-zqU" secondAttribute="trailing" id="mZ2-XG-Kvg"/>
<constraint firstItem="Ds3-59-ooT" firstAttribute="centerY" secondItem="ejl-zC-eNy" secondAttribute="centerY" id="tIh-lb-KbY"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Ds3-59-ooT" secondAttribute="trailing" id="vSU-N6-Sk5"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="label" destination="7mJ-VZ-zqU" id="MHr-r4-qop"/>
<outlet property="progressView" destination="Ds3-59-ooT" id="TjM-db-LxM"/>
</connections>
<point key="canvasLocation" x="-75" y="-117"/>
</view>
</objects>
<resources>
<systemColor name="secondaryLabelColor">
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>