diff --git a/Core/Core/Common/CommonModels/AppEnvironment/BrandStyle/Brand.swift b/Core/Core/Common/CommonModels/AppEnvironment/BrandStyle/Brand.swift
index f65b497091..fa744507c1 100644
--- a/Core/Core/Common/CommonModels/AppEnvironment/BrandStyle/Brand.swift
+++ b/Core/Core/Common/CommonModels/AppEnvironment/BrandStyle/Brand.swift
@@ -241,14 +241,28 @@ public struct Brand: Equatable {
}
}
- public func headerImageView() -> UIImageView {
- let logoView = UIImageView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
- logoView.contentMode = .scaleAspectFit
- logoView.heightAnchor.constraint(equalToConstant: 44).isActive = true
- logoView.widthAnchor.constraint(equalToConstant: 44).isActive = true
- logoView.image = headerImage
- logoView.backgroundColor = headerImageBackground
- logoView.accessibilityElementsHidden = true
- return logoView
- }
+ @available(iOS, deprecated: 26, message: "Remove conditional code when iOS 18 support is dropped")
+ public func headerImageView() -> UIImageView {
+ if #available(iOS 26, *) {
+ let logoView = UIImageView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
+ logoView.contentMode = .scaleAspectFit
+ logoView.heightAnchor.constraint(equalToConstant: 44).isActive = true
+ logoView.widthAnchor.constraint(equalToConstant: 44).isActive = true
+ logoView.image = headerImage
+ logoView.backgroundColor = headerImageBackground
+ logoView.layer.cornerRadius = 8
+ logoView.clipsToBounds = true
+ logoView.accessibilityElementsHidden = true
+ return logoView
+ } else {
+ let logoView = UIImageView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
+ logoView.contentMode = .scaleAspectFit
+ logoView.heightAnchor.constraint(equalToConstant: 44).isActive = true
+ logoView.widthAnchor.constraint(equalToConstant: 44).isActive = true
+ logoView.image = headerImage
+ logoView.backgroundColor = headerImageBackground
+ logoView.accessibilityElementsHidden = true
+ return logoView
+ }
+ }
}
diff --git a/Core/Core/Common/CommonModels/Router/Router.swift b/Core/Core/Common/CommonModels/Router/Router.swift
index 8f0445ce81..3d4d87a2ff 100644
--- a/Core/Core/Common/CommonModels/Router/Router.swift
+++ b/Core/Core/Common/CommonModels/Router/Router.swift
@@ -257,7 +257,7 @@ open class Router {
if view is UIAlertController { return from.present(view, animated: true, completion: completion) }
- if let displayModeButton = from.splitDisplayModeButtonItem,
+ if let displayModeButton = from.splitDisplayModeButtonItem, #unavailable(iOS 26),
from.splitViewController?.isCollapsed == false,
options.isDetail || from.isInSplitViewDetail,
!options.isModal {
diff --git a/Core/Core/Common/CommonUI/EmbeddedWebPage/View/EmbeddedWebPageContainerScreen.swift b/Core/Core/Common/CommonUI/EmbeddedWebPage/View/EmbeddedWebPageContainerScreen.swift
index 42ee7d2d80..19b791be74 100644
--- a/Core/Core/Common/CommonUI/EmbeddedWebPage/View/EmbeddedWebPageContainerScreen.swift
+++ b/Core/Core/Common/CommonUI/EmbeddedWebPage/View/EmbeddedWebPageContainerScreen.swift
@@ -40,22 +40,42 @@ public struct EmbeddedWebPageContainerScreen: View {
}
public var body: some View {
- WebSession(url: viewModel.url) { sessionURL in
- WebView(
- url: sessionURL,
- features: features,
- canToggleTheme: true,
- configuration: viewModel.webViewConfig
- )
- .onProvisionalNavigationStarted { webView, navigation in
- viewModel.webView(webView, didStartProvisionalNavigation: navigation)
+ if #available(iOS 26, *) {
+ WebSession(url: viewModel.url) { sessionURL in
+ WebView(
+ url: sessionURL,
+ features: features,
+ canToggleTheme: true,
+ configuration: viewModel.webViewConfig
+ )
+ .onProvisionalNavigationStarted { webView, navigation in
+ viewModel.webView(webView, didStartProvisionalNavigation: navigation)
+ }
+ .navBarItems(leading: viewModel.leadingNavigationButton)
+ .onAppear {
+ viewModel.viewController = viewController.value
+ }
}
- .navBarItems(leading: viewModel.leadingNavigationButton)
- .onAppear {
- viewModel.viewController = viewController.value
+ .navigationTitle(viewModel.navTitle)
+ .optionalNavigationSubtitle(viewModel.subTitle)
+ } else {
+ WebSession(url: viewModel.url) { sessionURL in
+ WebView(
+ url: sessionURL,
+ features: features,
+ canToggleTheme: true,
+ configuration: viewModel.webViewConfig
+ )
+ .onProvisionalNavigationStarted { webView, navigation in
+ viewModel.webView(webView, didStartProvisionalNavigation: navigation)
+ }
+ .navBarItems(leading: viewModel.leadingNavigationButton)
+ .onAppear {
+ viewModel.viewController = viewController.value
+ }
}
+ .navigationBarTitleView(title: viewModel.navTitle, subtitle: viewModel.subTitle)
+ .navigationBarStyle(.color(viewModel.contextColor))
}
- .navigationBarTitleView(title: viewModel.navTitle, subtitle: viewModel.subTitle)
- .navigationBarStyle(.color(viewModel.contextColor))
}
}
diff --git a/Core/Core/Common/CommonUI/InstUI/Views/NavigationBarTitleView.swift b/Core/Core/Common/CommonUI/InstUI/Views/NavigationBarTitleView.swift
index 0ecb8023c4..ac0b02b98e 100644
--- a/Core/Core/Common/CommonUI/InstUI/Views/NavigationBarTitleView.swift
+++ b/Core/Core/Common/CommonUI/InstUI/Views/NavigationBarTitleView.swift
@@ -20,6 +20,7 @@ import SwiftUI
extension InstUI {
+ @available(iOS, deprecated: 26)
public struct NavigationBarTitleView: View {
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
@Environment(\.navBarColors) private var navBarColors
diff --git a/Core/Core/Common/CommonUI/NavigationBar/SwiftUI/NavigationBarViewModifiers.swift b/Core/Core/Common/CommonUI/NavigationBar/SwiftUI/NavigationBarViewModifiers.swift
index 77a420aec4..02a331bb32 100644
--- a/Core/Core/Common/CommonUI/NavigationBar/SwiftUI/NavigationBarViewModifiers.swift
+++ b/Core/Core/Common/CommonUI/NavigationBar/SwiftUI/NavigationBarViewModifiers.swift
@@ -18,10 +18,12 @@
import SwiftUI
+@available(iOS, deprecated: 26)
protocol NavigationBarStyled: AnyObject {
var navigationBarStyle: NavigationBarStyle { get set }
}
+@available(iOS, deprecated: 26)
struct NavigationBarStyleModifier: ViewModifier {
let style: NavigationBarStyle
@@ -47,6 +49,7 @@ struct RightBarButtonItemModifier: ViewModifier {
}
}
+@available(iOS, deprecated: 26)
struct GlobalNavigationBarModifier: ViewModifier {
@Environment(\.viewController) var controller
@@ -56,6 +59,7 @@ struct GlobalNavigationBarModifier: ViewModifier {
}
}
+@available(iOS, deprecated: 26)
struct NavBarBackButtonModifier: ViewModifier {
@Environment(\.viewController) private var controller
@@ -67,6 +71,26 @@ struct NavBarBackButtonModifier: ViewModifier {
}
extension View {
+ @available(iOS, introduced: 26)
+ @ViewBuilder
+ public func optionalNavigationSubtitle(_ subtitle: String?) -> some View {
+ if let subtitle {
+ self.navigationSubtitle(subtitle)
+ } else {
+ self
+ }
+ }
+
+ @available(iOS, introduced: 26)
+ @ViewBuilder
+ public func optionalNavigationTitle(_ title: String?) -> some View {
+ if let title {
+ self.navigationTitle(title)
+ } else {
+ self
+ }
+ }
+
/// Sets the navigation bar's background color, title color & font, button color & font.
/// - Warning: Make sure to call this method AFTER calling `navigationBarTitleView()` to affect it.
/// - Parameters:
@@ -75,6 +99,7 @@ extension View {
/// - `.modal` is primarily used on modal screens, but also on some screen which doesn't belong to a context, but not considered global.
/// - `.color()` is used on non-modal screens within a context (typically a course or group), and in some other cases.
/// - Use `.color(nil)` to keep the navigation bar's current context background color but ensure the proper title color is set.
+ @available(iOS, deprecated: 26)
public func navigationBarStyle(_ style: NavigationBarStyle) -> some View {
modifier(NavigationBarStyleModifier(style: style))
}
@@ -84,6 +109,7 @@ extension View {
/// - Parameters:
/// - title: The line is always displayed, even if this is empty. (This should not happen normally.)
/// - subtitle: The subtitle line is only displayed if this is not empty.
+ @available(iOS, deprecated: 26)
public func navigationBarTitleView(title: String, subtitle: String?) -> some View {
toolbar {
ToolbarItem(placement: .principal) {
@@ -94,12 +120,14 @@ extension View {
/// Sets the navigation bar's title, using the proper font. Please use this one instead of the native `navigationTitle()` method.
/// - Warning: Make sure to call `navigationBarStyle()` _**AFTER**_ this method to set the proper text color.
+ @available(iOS, deprecated: 26)
public func navigationBarTitleView(_ title: String) -> some View {
navigationBarTitleView(title: title, subtitle: nil)
}
/// Sets the navigation bar's background color, button color to match the `Brand.shared` colors,
/// sets the button font and sets the brand logo as the titleView.
+ @available(iOS, deprecated: 26)
public func navigationBarGlobal() -> some View {
modifier(GlobalNavigationBarModifier())
}
diff --git a/Core/Core/Common/CommonUI/NavigationBar/UIKit/ColoredNavViewProtocol.swift b/Core/Core/Common/CommonUI/NavigationBar/UIKit/ColoredNavViewProtocol.swift
index 9c3a8c7608..972381b5c4 100644
--- a/Core/Core/Common/CommonUI/NavigationBar/UIKit/ColoredNavViewProtocol.swift
+++ b/Core/Core/Common/CommonUI/NavigationBar/UIKit/ColoredNavViewProtocol.swift
@@ -18,6 +18,7 @@
import UIKit
+@available(iOS, deprecated: 26)
public protocol ColoredNavViewProtocol: AnyObject {
var color: UIColor? { get set }
var navigationController: UINavigationController? { get }
@@ -28,13 +29,19 @@ public protocol ColoredNavViewProtocol: AnyObject {
}
extension ColoredNavViewProtocol {
+ @available(iOS, deprecated: 26)
public func setupTitleViewInNavbar(title: String) {
+ guard #unavailable(iOS 26) else { return }
+
navigationItem.titleView = titleSubtitleView
titleSubtitleView.title = title
titleSubtitleView.accessibilityTraits = .header
}
+ @available(iOS, deprecated: 26)
public func updateNavBar(subtitle: String?, color: UIColor?) {
+ guard #unavailable(iOS 26) else { return }
+
self.color = color
titleSubtitleView.subtitle = subtitle
navigationController?.navigationBar.useContextColor(color)
diff --git a/Core/Core/Common/CommonUI/NavigationBar/UIKit/TitleSubtitleView.swift b/Core/Core/Common/CommonUI/NavigationBar/UIKit/TitleSubtitleView.swift
index e4a85fee92..9b87a82baf 100644
--- a/Core/Core/Common/CommonUI/NavigationBar/UIKit/TitleSubtitleView.swift
+++ b/Core/Core/Common/CommonUI/NavigationBar/UIKit/TitleSubtitleView.swift
@@ -18,6 +18,7 @@
import UIKit
+@available(iOS, deprecated: 26)
public class TitleSubtitleView: UIView {
@IBOutlet public weak var titleLabel: UILabel!
@IBOutlet public weak var subtitleLabel: UILabel!
@@ -51,27 +52,37 @@ public class TitleSubtitleView: UIView {
}
public static func create() -> Self {
- let view = loadFromXib()
- view.titleLabel.text = ""
- view.subtitleLabel.text = ""
- view.titleLabel.font = .scaledNamedFont(.semibold16)
- view.subtitleLabel.font = .scaledNamedFont(.regular14)
- view.titleLabel.accessibilityElementsHidden = true
- view.subtitleLabel.accessibilityElementsHidden = true
- view.accessibilityTraits = [.header]
- view.showsLargeContentViewer = true
- view.addInteraction(UILargeContentViewerInteraction())
- return view
+ let view = loadFromXib()
+
+ if #available(iOS 26, *) {
+ view.tintColor = .textDarkest
+ }
+
+ view.titleLabel.text = ""
+ view.subtitleLabel.text = ""
+ view.titleLabel.font = .scaledNamedFont(.semibold16)
+ view.subtitleLabel.font = .scaledNamedFont(.regular14)
+ view.titleLabel.accessibilityElementsHidden = true
+ view.subtitleLabel.accessibilityElementsHidden = true
+ view.accessibilityTraits = [.header]
+ view.showsLargeContentViewer = true
+ view.addInteraction(UILargeContentViewerInteraction())
+ return view
}
public func recreate() -> TitleSubtitleView {
let copy = TitleSubtitleView.create()
+
+ if #available(iOS 26, *) {
+ copy.tintColor = .textDarkest
+ }
copy.title = title
copy.subtitle = subtitle
return copy
}
public override func tintColorDidChange() {
+ guard #available(iOS 26, *) else { return }
let title = (superview?.superview as? UINavigationBar)?.titleTextAttributes?[.foregroundColor] as? UIColor
let color = title ?? tintColor
titleLabel.textColor = color
diff --git a/Core/Core/Common/CommonUI/NavigationBar/UIKit/UINavigationBarExtensions.swift b/Core/Core/Common/CommonUI/NavigationBar/UIKit/UINavigationBarExtensions.swift
index e9003865cb..3ed530e6a9 100644
--- a/Core/Core/Common/CommonUI/NavigationBar/UIKit/UINavigationBarExtensions.swift
+++ b/Core/Core/Common/CommonUI/NavigationBar/UIKit/UINavigationBarExtensions.swift
@@ -33,8 +33,9 @@ extension UINavigationBar {
}
}
+ @available(iOS, deprecated: 26)
public func useContextColor(_ color: UIColor?) {
- guard let color else { return }
+ guard let color, #unavailable(iOS 26) else { return }
let foreground = UIColor.textLightest
let background = color
titleTextAttributes = [.foregroundColor: foreground]
@@ -45,7 +46,10 @@ extension UINavigationBar {
applyAppearanceChanges(backgroundColor: background, foregroundColor: foreground)
}
+ @available(iOS, deprecated: 26)
public func useGlobalNavStyle(brand: Brand = Brand.shared) {
+ guard #unavailable(iOS 26) else { return }
+
// TODO: Remove the isHorizon condition once horizon-specific logic is no longer needed.
let isHorizon = AppEnvironment.shared.app == .horizon
let background: UIColor = isHorizon ? .backgroundLightest : brand.navBackground
diff --git a/Core/Core/Common/CommonUI/UIViews/CoreSplitViewController.swift b/Core/Core/Common/CommonUI/UIViews/CoreSplitViewController.swift
index 6cf0c49fa1..08b1289210 100644
--- a/Core/Core/Common/CommonUI/UIViews/CoreSplitViewController.swift
+++ b/Core/Core/Common/CommonUI/UIViews/CoreSplitViewController.swift
@@ -98,6 +98,7 @@ public class CoreSplitViewController: UISplitViewController {
}
}
+ @available(iOS, deprecated: 26, message: "iOS26 has a default implementation")
public func prettyDisplayModeButtonItem(_ displayMode: DisplayMode) -> UIBarButtonItem {
let defaultButton = self.displayModeButtonItem
let collapse = displayMode == .oneOverSecondary || displayMode == .secondaryOnly
@@ -121,9 +122,13 @@ extension CoreSplitViewController: UISplitViewControllerDelegate {
public func splitViewController(_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode) {
if svc.viewControllers.count == 2 {
let top = (svc.viewControllers.last as? UINavigationController)?.topViewController
- top?.navigationItem.leftItemsSupplementBackButton = true
+ if #unavailable(iOS 26) {
+ top?.navigationItem.leftItemsSupplementBackButton = true
+ }
if top?.isKind(of: EmptyViewController.self) == false {
- top?.navigationItem.leftBarButtonItem = prettyDisplayModeButtonItem(displayMode)
+ if #unavailable(iOS 26) {
+ top?.navigationItem.leftBarButtonItem = prettyDisplayModeButtonItem(displayMode)
+ }
NotificationCenter.default.post(name: NSNotification.Name.SplitViewControllerWillChangeDisplayModeNotification, object: self)
}
}
@@ -179,9 +184,12 @@ extension CoreSplitViewController: UISplitViewControllerDelegate {
}
let viewControllers = (newDeets as? UINavigationController)?.viewControllers ?? [newDeets]
- for vc in viewControllers {
- vc.navigationItem.leftItemsSupplementBackButton = true
- vc.navigationItem.leftBarButtonItem = prettyDisplayModeButtonItem(splitViewController.displayMode)
+
+ if #unavailable(iOS 26) {
+ for vc in viewControllers {
+ vc.navigationItem.leftItemsSupplementBackButton = true
+ vc.navigationItem.leftBarButtonItem = prettyDisplayModeButtonItem(splitViewController.displayMode)
+ }
}
if let nav = newDeets as? UINavigationController {
diff --git a/Core/Core/Common/CommonUI/UIViews/EmptyViewController.swift b/Core/Core/Common/CommonUI/UIViews/EmptyViewController.swift
index e7d2cc5c5a..550c5f4209 100644
--- a/Core/Core/Common/CommonUI/UIViews/EmptyViewController.swift
+++ b/Core/Core/Common/CommonUI/UIViews/EmptyViewController.swift
@@ -30,9 +30,9 @@ public class EmptyViewController: UIViewController {
logoImageView.tintColor = .textDark
logoImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
- logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
- logoImageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
- logoImageView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.1),
+ logoImageView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
+ logoImageView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor),
+ logoImageView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, multiplier: 0.2),
logoImageView.heightAnchor.constraint(equalTo: logoImageView.widthAnchor)
])
}
diff --git a/Core/Core/Common/CommonUI/UIViews/HorizontalMenuViewController.swift b/Core/Core/Common/CommonUI/UIViews/HorizontalMenuViewController.swift
index 4e9e083d0b..03e2600f3b 100644
--- a/Core/Core/Common/CommonUI/UIViews/HorizontalMenuViewController.swift
+++ b/Core/Core/Common/CommonUI/UIViews/HorizontalMenuViewController.swift
@@ -32,7 +32,7 @@ open class HorizontalMenuViewController: ScreenViewTrackerViewController {
public weak var delegate: HorizontalPagedMenuDelegate?
public private(set) var selectedIndexPath: IndexPath = IndexPath(item: 0, section: 0)
- private var itemCount: Int {
+ public var itemCount: Int {
return delegate?.numberOfMenuItems ?? 0
}
@@ -78,8 +78,16 @@ open class HorizontalMenuViewController: ScreenViewTrackerViewController {
} else {
setupMenu()
setupPages()
- setupUnderline()
setupBottomBorder()
+
+ // Only show the underline on iOS 26 if there are more than one tabs
+ if #available(iOS 26, *) {
+ if itemCount > 1 {
+ setupUnderline()
+ }
+ } else {
+ setupUnderline()
+ }
}
}
diff --git a/Core/Core/Common/CommonUI/SwiftUIViews/ListWithoutVerticalScrollIndicator.swift b/Core/Core/Common/CommonUI/UIViews/LegacySectionHeaderView.swift
similarity index 55%
rename from Core/Core/Common/CommonUI/SwiftUIViews/ListWithoutVerticalScrollIndicator.swift
rename to Core/Core/Common/CommonUI/UIViews/LegacySectionHeaderView.swift
index bc0cefa5cc..2cf39829aa 100644
--- a/Core/Core/Common/CommonUI/SwiftUIViews/ListWithoutVerticalScrollIndicator.swift
+++ b/Core/Core/Common/CommonUI/UIViews/LegacySectionHeaderView.swift
@@ -1,6 +1,6 @@
//
// This file is part of Canvas.
-// Copyright (C) 2022-present Instructure, Inc.
+// Copyright (C) 2019-present Instructure, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
@@ -16,20 +16,21 @@
// along with this program. If not, see .
//
-import SwiftUI
+import UIKit
-public struct ListWithoutVerticalScrollIndicator: View {
- private let scrollIndicatorWidth: CGFloat = 6
- private let content: () -> Content
+@available(iOS, deprecated: 26, message: "Non-legacy version exists")
+public class LegacySectionHeaderView: UITableViewHeaderFooterView {
+ @IBOutlet weak public var titleLabel: UILabel!
+ @IBOutlet weak var bgView: UIView!
- public init(content: @escaping () -> Content) {
- self.content = content
+ public override func awakeFromNib() {
+ super.awakeFromNib()
+ bgView.backgroundColor = .backgroundLight
}
- public var body: some View {
- List {
- content().padding(.horizontal, scrollIndicatorWidth)
- }
- .padding(.horizontal, -scrollIndicatorWidth)
+ public static func create(title: String, section: Int) -> LegacySectionHeaderView {
+ let view = loadFromXib()
+ view.titleLabel.text = title
+ return view
}
}
diff --git a/Core/Core/Common/CommonUI/UIViews/LegacySectionHeaderView.xib b/Core/Core/Common/CommonUI/UIViews/LegacySectionHeaderView.xib
new file mode 100644
index 0000000000..6e787d8fe1
--- /dev/null
+++ b/Core/Core/Common/CommonUI/UIViews/LegacySectionHeaderView.xib
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Core/Core/Common/CommonUI/UIViews/SectionHeaderView.swift b/Core/Core/Common/CommonUI/UIViews/SectionHeaderView.swift
index d138e5bc55..c187cf3af6 100644
--- a/Core/Core/Common/CommonUI/UIViews/SectionHeaderView.swift
+++ b/Core/Core/Common/CommonUI/UIViews/SectionHeaderView.swift
@@ -18,14 +18,9 @@
import UIKit
+@available(iOS, introduced: 26, message: "Legacy version exists")
public class SectionHeaderView: UITableViewHeaderFooterView {
@IBOutlet weak public var titleLabel: UILabel!
- @IBOutlet weak var bgView: UIView!
-
- public override func awakeFromNib() {
- super.awakeFromNib()
- bgView.backgroundColor = .backgroundLight
- }
public static func create(title: String, section: Int) -> SectionHeaderView {
let view = loadFromXib()
diff --git a/Core/Core/Common/CommonUI/UIViews/SectionHeaderView.xib b/Core/Core/Common/CommonUI/UIViews/SectionHeaderView.xib
index d1c4b583ef..aa00c1a84e 100644
--- a/Core/Core/Common/CommonUI/UIViews/SectionHeaderView.xib
+++ b/Core/Core/Common/CommonUI/UIViews/SectionHeaderView.xib
@@ -1,9 +1,8 @@
-
+
-
-
+
@@ -14,21 +13,8 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Core/Core/Common/CommonUI/ViewModifiers/Tint.swift b/Core/Core/Common/CommonUI/ViewModifiers/Tint.swift
index 631e10fc2a..0c01f7c4be 100644
--- a/Core/Core/Common/CommonUI/ViewModifiers/Tint.swift
+++ b/Core/Core/Common/CommonUI/ViewModifiers/Tint.swift
@@ -34,4 +34,24 @@ extension View {
public func applyTint() -> some View {
foregroundStyle(.tint)
}
+
+ @available(iOS, deprecated: 26, message: "Intended for temporary compatibility before full iOS 26 adoption.")
+ @ViewBuilder
+ public func tintBelow26(_ tint: Color?) -> some View {
+ if #available(iOS 26, *) {
+ self
+ } else {
+ self.tint(tint)
+ }
+ }
+
+ @available(iOS, deprecated: 26, message: "Intended for temporary compatibility before full iOS 26 adoption.")
+ @ViewBuilder
+ public func foregroundStyleBelow26(_ style: S) -> some View where S: ShapeStyle {
+ if #available(iOS 26, *) {
+ self
+ } else {
+ self.foregroundStyle(style)
+ }
+ }
}
diff --git a/Core/Core/Common/Extensions/InstIconExtensions.swift b/Core/Core/Common/Extensions/InstIconExtensions.swift
index 07b38f9626..a314c34fd7 100644
--- a/Core/Core/Common/Extensions/InstIconExtensions.swift
+++ b/Core/Core/Common/Extensions/InstIconExtensions.swift
@@ -40,6 +40,7 @@ public extension UIImage {
static var arrowOpenRightSolid: UIImage { UIImage(named: "arrowOpenRightSolid", in: .core, compatibleWith: nil)! }
static var arrowOpenUpLine: UIImage { UIImage(named: "arrowOpenUpLine", in: .core, compatibleWith: nil)! }
static var arrowOpenUpSolid: UIImage { UIImage(named: "arrowOpenUpSolid", in: .core, compatibleWith: nil)! }
+ static var arrowUpLine: UIImage { UIImage(named: "arrowUpLine", in: .core, compatibleWith: nil)! }
static var assignmentLine: UIImage { UIImage(named: "assignmentLine", in: .core, compatibleWith: nil)! }
static var assignmentSolid: UIImage { UIImage(named: "assignmentSolid", in: .core, compatibleWith: nil)! }
static var audioLine: UIImage { UIImage(named: "audioLine", in: .core, compatibleWith: nil)! }
@@ -307,6 +308,7 @@ public extension Image {
static var arrowOpenRightSolid: Image { Image("arrowOpenRightSolid", bundle: .core) }
static var arrowOpenUpLine: Image { Image("arrowOpenUpLine", bundle: .core) }
static var arrowOpenUpSolid: Image { Image("arrowOpenUpSolid", bundle: .core) }
+ static var arrowUpLine: Image { Image("arrowUpLine", bundle: .core) }
static var assignmentLine: Image { Image("assignmentLine", bundle: .core) }
static var assignmentSolid: Image { Image("assignmentSolid", bundle: .core) }
static var audioLine: Image { Image("audioLine", bundle: .core) }
diff --git a/Core/Core/Common/Extensions/UIKit/UIBarButtonItemExtensions.swift b/Core/Core/Common/Extensions/UIKit/UIBarButtonItemExtensions.swift
index 48798f6348..62bdd25b37 100644
--- a/Core/Core/Common/Extensions/UIKit/UIBarButtonItemExtensions.swift
+++ b/Core/Core/Common/Extensions/UIKit/UIBarButtonItemExtensions.swift
@@ -28,16 +28,28 @@ public extension UIBarButtonItem {
}
static func back(actionHandler: @escaping () -> Void) -> UIBarButtonItem {
- let config = UIImage.SymbolConfiguration(weight: .semibold)
- let backImage = UIImage(systemName: "chevron.backward", withConfiguration: config)
- let barButton = UIBarButtonItemWithCompletion(
- image: backImage,
- landscapeImagePhone: backImage,
- style: .plain,
- actionHandler: actionHandler
- )
- barButton.imageInsets = .init(top: 0, left: -7.5, bottom: 0, right: 0)
- barButton.landscapeImagePhoneInsets = .init(top: 0, left: -8, bottom: 0, right: 0)
- return barButton
+ if #available(iOS 26, *) {
+ let backImage = UIImage(systemName: "chevron.backward")
+ let barButton = UIBarButtonItemWithCompletion(
+ image: backImage,
+ landscapeImagePhone: backImage,
+ style: .plain,
+ actionHandler: actionHandler
+ )
+ barButton.landscapeImagePhoneInsets = .init(top: 0, left: -8, bottom: 0, right: 0)
+ return barButton
+ } else {
+ let config = UIImage.SymbolConfiguration(weight: .semibold)
+ let backImage = UIImage(systemName: "chevron.backward", withConfiguration: config)
+ let barButton = UIBarButtonItemWithCompletion(
+ image: backImage,
+ landscapeImagePhone: backImage,
+ style: .plain,
+ actionHandler: actionHandler
+ )
+ barButton.imageInsets = .init(top: 0, left: -7.5, bottom: 0, right: 0)
+ barButton.landscapeImagePhoneInsets = .init(top: 0, left: -8, bottom: 0, right: 0)
+ return barButton
+ }
}
}
diff --git a/Core/Core/Common/Extensions/UIKit/UIViewControllerExtensions.swift b/Core/Core/Common/Extensions/UIKit/UIViewControllerExtensions.swift
index 2fa485ed5f..70f6256089 100644
--- a/Core/Core/Common/Extensions/UIKit/UIViewControllerExtensions.swift
+++ b/Core/Core/Common/Extensions/UIKit/UIViewControllerExtensions.swift
@@ -57,6 +57,7 @@ extension UIViewController {
return false
}
+ @available(iOS, deprecated: 26)
public var splitDisplayModeButtonItem: UIBarButtonItem? {
guard let splitView = splitViewController else { return nil }
let defaultButton = splitView.displayModeButtonItem
diff --git a/Core/Core/Features/Assignments/AssignmentList/View/AssignmentListScreen.swift b/Core/Core/Features/Assignments/AssignmentList/View/AssignmentListScreen.swift
index 1f74ec2d0e..15e6487135 100644
--- a/Core/Core/Features/Assignments/AssignmentList/View/AssignmentListScreen.swift
+++ b/Core/Core/Features/Assignments/AssignmentList/View/AssignmentListScreen.swift
@@ -36,33 +36,60 @@ public struct AssignmentListScreen: View, ScreenViewTrackable {
}
public var body: some View {
- VStack(spacing: 0) {
- switch viewModel.state {
- case .empty, .error:
- gradingPeriodTitle
- emptyPanda
- case .loading:
- loadingView
- case .data:
- gradingPeriodTitle
- assignmentList
- }
- }
- .background(Color.backgroundLightest.edgesIgnoringSafeArea(.all))
- .tint(viewModel.courseColor?.asColor)
- .navigationBarTitleView(
- title: String(localized: "Assignments", bundle: .core),
- subtitle: viewModel.courseName
- )
- .navigationBarGenericBackButton()
- .navBarItems(
- trailing: .filterIcon(isBackgroundContextColor: true, isSolid: viewModel.isFilterIconSolid) {
- viewModel.navigateToPreferences(viewController: controller)
- }
- )
- .navigationBarStyle(.color(viewModel.courseColor))
- .onAppear(perform: viewModel.viewDidAppear)
- .onReceive(viewModel.$defaultDetailViewRoute, perform: setupDefaultSplitDetailView)
+ if #available(iOS 26, *) {
+ VStack(spacing: 0) {
+ switch viewModel.state {
+ case .empty, .error:
+ gradingPeriodTitle
+ emptyPanda
+ case .loading:
+ loadingView
+ case .data:
+ assignmentList
+ }
+ }
+ .navigationTitle(.init("Assignments", bundle: .core))
+ .optionalNavigationSubtitle(viewModel.courseName)
+ .background(Color.backgroundLightest.edgesIgnoringSafeArea(.all))
+ .tint(viewModel.courseColor?.asColor)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ InstUI.NavigationBarButton.filterIcon(isSolid: viewModel.isFilterIconSolid) {
+ viewModel.navigateToPreferences(viewController: controller)
+ }
+ }
+ }
+ .onAppear(perform: viewModel.viewDidAppear)
+ .onReceive(viewModel.$defaultDetailViewRoute, perform: setupDefaultSplitDetailView)
+ } else {
+ VStack(spacing: 0) {
+ switch viewModel.state {
+ case .empty, .error:
+ gradingPeriodTitle
+ emptyPanda
+ case .loading:
+ loadingView
+ case .data:
+ gradingPeriodTitle
+ assignmentList
+ }
+ }
+ .background(Color.backgroundLightest.edgesIgnoringSafeArea(.all))
+ .tint(viewModel.courseColor?.asColor)
+ .navigationBarTitleView(
+ title: String(localized: "Assignments", bundle: .core),
+ subtitle: viewModel.courseName
+ )
+ .navigationBarGenericBackButton()
+ .navBarItems(
+ trailing: .filterIcon(isBackgroundContextColor: true, isSolid: viewModel.isFilterIconSolid) {
+ viewModel.navigateToPreferences(viewController: controller)
+ }
+ )
+ .navigationBarStyle(.color(viewModel.courseColor))
+ .onAppear(perform: viewModel.viewDidAppear)
+ .onReceive(viewModel.$defaultDetailViewRoute, perform: setupDefaultSplitDetailView)
+ }
}
private var gradingPeriodTitle: some View {
diff --git a/Core/Core/Features/Assignments/AssignmentList/View/AssignmentListView.swift b/Core/Core/Features/Assignments/AssignmentList/View/AssignmentListView.swift
index 10b68c06f5..eaac0c449a 100644
--- a/Core/Core/Features/Assignments/AssignmentList/View/AssignmentListView.swift
+++ b/Core/Core/Features/Assignments/AssignmentList/View/AssignmentListView.swift
@@ -42,9 +42,17 @@ struct AssignmentListView: View {
}
var body: some View {
- LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
- ForEach(sections) { section in
- sectionView(with: section)
+ if #available(iOS 26, *) {
+ LazyVStack(spacing: 0) {
+ ForEach(sections) { section in
+ sectionView(with: section)
+ }
+ }
+ } else {
+ LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
+ ForEach(sections) { section in
+ sectionView(with: section)
+ }
}
}
}
diff --git a/Core/Core/Features/Conferences/ConferenceDetailsViewController.swift b/Core/Core/Features/Conferences/ConferenceDetailsViewController.swift
index 458e68ddfc..8f70085ccf 100644
--- a/Core/Core/Features/Conferences/ConferenceDetailsViewController.swift
+++ b/Core/Core/Features/Conferences/ConferenceDetailsViewController.swift
@@ -72,7 +72,11 @@ public class ConferenceDetailsViewController: ScreenViewTrackableViewController,
view.backgroundColor = .backgroundLightest
tableView.backgroundColor = .backgroundLightest
- setupTitleViewInNavbar(title: String(localized: "Conference Details", bundle: .core))
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Conference Details", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Conference Details", bundle: .core))
+ }
detailsHeadingLabel.text = String(localized: "Description", bundle: .core)
@@ -109,7 +113,11 @@ public class ConferenceDetailsViewController: ScreenViewTrackableViewController,
}
spinnerView.color = color
refreshControl.color = color
- updateNavBar(subtitle: name, color: color)
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = name
+ } else {
+ updateNavBar(subtitle: name, color: color)
+ }
}
func update() {
diff --git a/Core/Core/Features/Conferences/ConferenceListViewController.swift b/Core/Core/Features/Conferences/ConferenceListViewController.swift
index 7563abbbd4..5b76fb60a3 100644
--- a/Core/Core/Features/Conferences/ConferenceListViewController.swift
+++ b/Core/Core/Features/Conferences/ConferenceListViewController.swift
@@ -57,7 +57,12 @@ public class ConferenceListViewController: ScreenViewTrackableViewController, Co
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .backgroundLightest
- setupTitleViewInNavbar(title: String(localized: "Conferences", bundle: .core))
+
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Conferences", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Conferences", bundle: .core))
+ }
emptyMessageLabel.text = String(localized: "There are no conferences to display yet.", bundle: .core)
emptyTitleLabel.text = String(localized: "No Conferences", bundle: .core)
@@ -67,7 +72,11 @@ public class ConferenceListViewController: ScreenViewTrackableViewController, Co
tableView.backgroundColor = .backgroundLightest
refreshControl.addTarget(self, action: #selector(refresh), for: .primaryActionTriggered)
tableView.refreshControl = refreshControl
- tableView.registerHeaderFooterView(SectionHeaderView.self)
+ if #available(iOS 26, *) {
+ tableView.registerHeaderFooterView(SectionHeaderView.self)
+ } else {
+ tableView.registerHeaderFooterView(LegacySectionHeaderView.self)
+ }
tableView.separatorColor = .borderMedium
colors.refresh()
@@ -84,7 +93,10 @@ public class ConferenceListViewController: ScreenViewTrackableViewController, Co
if let selected = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selected, animated: true)
}
- navigationController?.navigationBar.useContextColor(color)
+
+ if #unavailable(iOS 26) {
+ navigationController?.navigationBar.useContextColor(color)
+ }
}
func updateNavBar() {
@@ -97,7 +109,12 @@ public class ConferenceListViewController: ScreenViewTrackableViewController, Co
view.tintColor = color
spinnerView.color = color
refreshControl.color = color
- updateNavBar(subtitle: name, color: color)
+
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = name
+ } else {
+ updateNavBar(subtitle: name, color: color)
+ }
}
func update() {
@@ -128,12 +145,23 @@ extension ConferenceListViewController: UITableViewDataSource, UITableViewDelega
}
public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
- let view = tableView.dequeueHeaderFooter(SectionHeaderView.self)
- view.titleLabel?.text = conferences[IndexPath(row: 0, section: section)]?.isConcluded == true
- ? String(localized: "Concluded Conferences", bundle: .core)
- : String(localized: "New Conferences", bundle: .core)
- view.titleLabel?.accessibilityIdentifier = "ConferencesList.header-\(section)"
- return view
+ if #available(iOS 26, *) {
+ let view = tableView.dequeueHeaderFooter(SectionHeaderView.self)
+ view.titleLabel?.text = conferences[IndexPath(row: 0, section: section)]?.isConcluded == true
+ ? String(localized: "Concluded Conferences", bundle: .core)
+ : String(localized: "New Conferences", bundle: .core)
+ view.titleLabel?.accessibilityIdentifier = "ConferencesList.header-\(section)"
+
+ return view
+ } else {
+ let view = tableView.dequeueHeaderFooter(LegacySectionHeaderView.self)
+ view.titleLabel?.text = conferences[IndexPath(row: 0, section: section)]?.isConcluded == true
+ ? String(localized: "Concluded Conferences", bundle: .core)
+ : String(localized: "New Conferences", bundle: .core)
+ view.titleLabel?.accessibilityIdentifier = "ConferencesList.header-\(section)"
+
+ return view
+ }
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
diff --git a/Core/Core/Features/Courses/CourseDetails/View/CourseDetailsHeaderView.swift b/Core/Core/Features/Courses/CourseDetails/View/CourseDetailsHeaderView.swift
index 0c8949d142..990079da2a 100644
--- a/Core/Core/Features/Courses/CourseDetails/View/CourseDetailsHeaderView.swift
+++ b/Core/Core/Features/Courses/CourseDetails/View/CourseDetailsHeaderView.swift
@@ -18,6 +18,7 @@
import SwiftUI
+@available(iOS, introduced: 26, message: "Legacy version exists")
struct CourseDetailsHeaderView: View {
@ObservedObject private var viewModel: CourseDetailsHeaderViewModel
private let width: CGFloat
@@ -28,7 +29,7 @@ struct CourseDetailsHeaderView: View {
}
public var body: some View {
- ZStack {
+ ZStack(alignment: .bottomLeading) {
Color(viewModel.courseColor)
.frame(width: width, height: viewModel.height)
if let url = viewModel.imageURL {
@@ -36,7 +37,7 @@ struct CourseDetailsHeaderView: View {
.opacity(viewModel.imageOpacity)
.accessibility(hidden: true)
}
- VStack(spacing: 3) {
+ VStack(alignment: .leading, spacing: 3) {
Text(viewModel.courseName)
.font(.semibold23)
.accessibility(identifier: "course-details.title-lbl")
@@ -44,6 +45,7 @@ struct CourseDetailsHeaderView: View {
color: viewModel.courseTitleShadow.color,
radius: viewModel.courseTitleShadow.radius
)
+ .multilineTextAlignment(.leading)
Text(viewModel.termName)
.font(.semibold14)
.accessibility(identifier: "course-details.subtitle-lbl")
@@ -51,20 +53,20 @@ struct CourseDetailsHeaderView: View {
color: viewModel.courseTitleShadow.color,
radius: viewModel.courseTitleShadow.radius
)
+ .multilineTextAlignment(.leading)
}
.padding()
.multilineTextAlignment(.center)
.foregroundColor(.textLightest)
.opacity(viewModel.titleOpacity)
}
- .frame(height: viewModel.height)
+ .frame(width: width, height: viewModel.height)
.clipped()
- .offset(x: 0, y: viewModel.verticalOffset)
}
}
#if DEBUG
-
+@available(iOS, introduced: 26, message: "Legacy version exists")
struct CourseDetailsHeaderView_Previews: PreviewProvider {
private static let env = AppEnvironment.shared
private static let context = env.globalDatabase.viewContext
diff --git a/Core/Core/Features/Courses/CourseDetails/View/CourseDetailsView.swift b/Core/Core/Features/Courses/CourseDetails/View/CourseDetailsView.swift
index 3eba424404..3f22bda840 100644
--- a/Core/Core/Features/Courses/CourseDetails/View/CourseDetailsView.swift
+++ b/Core/Core/Features/Courses/CourseDetails/View/CourseDetailsView.swift
@@ -23,14 +23,17 @@ public struct CourseDetailsView: View, ScreenViewTrackable {
@Environment(\.appEnvironment) private var env
@Environment(\.viewController) private var controller
@ObservedObject private var viewModel: CourseDetailsViewModel
- @ObservedObject private var headerViewModel: CourseDetailsHeaderViewModel
@ObservedObject private var selectionViewModel: ListSelectionViewModel
+ @ObservedObject private var headerViewModel: CourseDetailsHeaderViewModel
+ @available(iOS, deprecated: 26)
+ @ObservedObject private var legacyHeaderViewModel: LegacyCourseDetailsHeaderViewModel
public let screenViewTrackingParameters: ScreenViewTrackingParameters
public init(viewModel: CourseDetailsViewModel) {
self.viewModel = viewModel
self.headerViewModel = viewModel.headerViewModel
+ self.legacyHeaderViewModel = viewModel.legacyHeaderViewModel
self.selectionViewModel = viewModel.selectionViewModel
screenViewTrackingParameters = ScreenViewTrackingParameters(
@@ -39,35 +42,79 @@ public struct CourseDetailsView: View, ScreenViewTrackable {
}
public var body: some View {
- GeometryReader { geometry in
- VStack(spacing: 0) {
- switch viewModel.state {
- case .empty(let title, let message):
- imageHeader(geometry: geometry)
- errorView(title: title, message: message)
- case .loading:
- imageHeader(geometry: geometry)
- loadingView
- case .data(let tabViewModels):
- tabList(tabViewModels, geometry: geometry)
+ if #available(iOS 26, *) {
+ GeometryReader { geometry in
+ VStack(spacing: 0) {
+ switch viewModel.state {
+ case .empty(let title, let message):
+ imageHeader(geometry: geometry)
+ errorView(title: title, message: message)
+ case .loading:
+ imageHeader(geometry: geometry)
+ loadingView
+ case .data(let tabViewModels):
+ tabList(tabViewModels, geometry: geometry)
+ }
}
- }
- .background(Color.backgroundLightest.edgesIgnoringSafeArea(.all))
- .navigationBarTitleView(viewModel.navigationBarTitle)
- .navigationBarGenericBackButton()
- .navigationBarItems(trailing: viewModel.showSettings ? settingsButton : nil)
- .navigationBarStyle(.color(viewModel.courseColor))
- .onAppear {
- viewModel.viewDidAppear()
- viewModel.splitModeObserver.splitViewController = controller.value.splitViewController
- }
- }
- .onPreferenceChange(ViewBoundsKey.self, perform: headerViewModel.scrollPositionChanged)
- .onReceive(viewModel.$homeRoute, perform: setupDefaultSplitDetailView)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .background(Color.backgroundLightest.edgesIgnoringSafeArea(.all))
+ .navigationTitle(viewModel.navigationBarTitle)
+ .toolbar {
+ if viewModel.showSettings {
+ settingsButton
+ }
+ }
+ .onAppear {
+ viewModel.viewDidAppear()
+ viewModel.splitModeObserver.splitViewController = controller.value.splitViewController
+ }
+ }
+ .onReceive(viewModel.$homeRoute, perform: setupDefaultSplitDetailView)
+ } else {
+ GeometryReader { geometry in
+ VStack(spacing: 0) {
+ switch viewModel.state {
+ case .empty(let title, let message):
+ legacyImageHeader(geometry: geometry)
+ errorView(title: title, message: message)
+ case .loading:
+ legacyImageHeader(geometry: geometry)
+ loadingView
+ case .data(let tabViewModels):
+ legacyTabList(tabViewModels, geometry: geometry)
+ }
+ }
+ .background(Color.backgroundLightest.edgesIgnoringSafeArea(.all))
+ .navigationBarTitleView(viewModel.navigationBarTitle)
+ .navigationBarGenericBackButton()
+ .navigationBarItems(trailing: viewModel.showSettings ? legacySettingsButton : nil)
+ .navigationBarStyle(.color(viewModel.courseColor))
+ .onAppear {
+ viewModel.viewDidAppear()
+ viewModel.splitModeObserver.splitViewController = controller.value.splitViewController
+ }
+ }
+ .onPreferenceChange(ViewBoundsKey.self, perform: legacyHeaderViewModel.scrollPositionChanged)
+ .onReceive(viewModel.$homeRoute, perform: setupDefaultSplitDetailView)
+ }
}
+ @ViewBuilder
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ private var settingsButton: some View {
+ Button {
+ if let url = viewModel.settingsRoute {
+ env.router.route(to: url, from: controller, options: .modal(.formSheet, isDismissable: false, embedInNav: true))
+ }
+ } label: {
+ Image.settingsSolid
+ }
+ .accessibility(label: Text("Edit course settings", bundle: .core))
+ }
+
@ViewBuilder
- private var settingsButton: some View {
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private var legacySettingsButton: some View {
Button {
if let url = viewModel.settingsRoute {
env.router.route(to: url, from: controller, options: .modal(.formSheet, isDismissable: false, embedInNav: true))
@@ -146,10 +193,39 @@ public struct CourseDetailsView: View, ScreenViewTrackable {
Spacer()
}
- private func tabList(_ tabViewModels: [CourseDetailsCellViewModel], geometry: GeometryProxy) -> some View {
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ private func tabList(_ tabViewModels: [CourseDetailsCellViewModel], geometry: GeometryProxy) -> some View {
+ ScrollView {
+ VStack(spacing: 0) {
+ imageHeader(geometry: geometry)
+ .padding(.bottom, 16)
+ if viewModel.showHome {
+ homeView
+ Divider()
+ }
+ ForEach(tabViewModels, id: \.id) { tabViewModel in
+ CourseDetailsCellView(viewModel: tabViewModel)
+ Divider()
+ }
+ }
+ .listRowInsets(EdgeInsets())
+ .listRowBackground(Color.clear)
+ .listRowSeparator(.hidden)
+ .background(Color.backgroundLightest)
+ }
+ .scrollIndicators(.hidden)
+ .listStyle(.plain)
+ .scrollContentBackground(.hidden)
+ .refreshable {
+ await viewModel.refresh()
+ }
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private func legacyTabList(_ tabViewModels: [CourseDetailsCellViewModel], geometry: GeometryProxy) -> some View {
ZStack(alignment: .top) {
- imageHeader(geometry: geometry)
- ListWithoutVerticalScrollIndicator {
+ legacyImageHeader(geometry: geometry)
+ ScrollView {
VStack(spacing: 0) {
if viewModel.showHome {
homeView
@@ -164,13 +240,14 @@ public struct CourseDetailsView: View, ScreenViewTrackable {
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.background(Color.backgroundLightest)
- .padding(.top, headerViewModel.visibleHeight)
+ .padding(.top, legacyHeaderViewModel.visibleHeight)
// Save the frame of the content so we can inspect its y position and move course image based on that
.transformAnchorPreference(key: ViewBoundsKey.self, value: .bounds) { preferences, bounds in
preferences = [.init(viewId: 0, bounds: geometry[bounds])]
}
}
.listStyle(.plain)
+ .scrollIndicators(.hidden)
.scrollContentBackground(.hidden)
.refreshable {
await viewModel.refresh()
@@ -178,10 +255,17 @@ public struct CourseDetailsView: View, ScreenViewTrackable {
}
}
- @ViewBuilder
+ @available(iOS, introduced: 26, message: "Legacy version exists")
private func imageHeader(geometry: GeometryProxy) -> some View {
- if headerViewModel.shouldShowHeader(in: geometry.size) {
- CourseDetailsHeaderView(viewModel: headerViewModel, width: geometry.size.width)
+ CourseDetailsHeaderView(viewModel: headerViewModel, width: geometry.size.width - 32)
+ .clipShape(RoundedRectangle(cornerRadius: 16))
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ @ViewBuilder
+ private func legacyImageHeader(geometry: GeometryProxy) -> some View {
+ if legacyHeaderViewModel.shouldShowHeader(in: geometry.size) {
+ LegacyCourseDetailsHeaderView(viewModel: legacyHeaderViewModel, width: geometry.size.width)
}
}
@@ -209,7 +293,14 @@ struct CourseDetailsView_Previews: PreviewProvider {
private static let env = AppEnvironment.shared
private static let context = env.globalDatabase.viewContext
private static var contentViewModel: CourseDetailsViewModel {
- let course = Course.save(.make(default_view: .assignments, term: .init(id: "1", name: "Default Term", start_at: nil, end_at: nil)), in: context)
+ let course = Course.save(
+ .make(
+ name: "Long name to brake the layout of the title",
+ default_view: .assignments,
+ term: .init(id: "1", name: "Default Term", start_at: nil, end_at: nil)
+ ),
+ in: context
+ )
let tab1: Tab = Tab(context: context)
tab1.save(.make(), in: context, context: .course("1"))
let tab2: Tab = Tab(context: context)
diff --git a/Core/Core/Features/Courses/CourseDetails/View/CourseSettingsView.swift b/Core/Core/Features/Courses/CourseDetails/View/CourseSettingsView.swift
index 52c702f07a..f7302c3d82 100644
--- a/Core/Core/Features/Courses/CourseDetails/View/CourseSettingsView.swift
+++ b/Core/Core/Features/Courses/CourseDetails/View/CourseSettingsView.swift
@@ -34,41 +34,99 @@ public struct CourseSettingsView: View, ScreenViewTrackable {
}
public var body: some View {
- GeometryReader { geometry in
- switch viewModel.state {
- case .loading:
- ProgressView()
- .progressViewStyle(.indeterminateCircle())
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- case .ready, .saving:
- editor(width: geometry.size.width)
+ if #available(iOS 26, *) {
+ GeometryReader { geometry in
+ switch viewModel.state {
+ case .loading:
+ ProgressView()
+ .progressViewStyle(.indeterminateCircle())
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ case .ready, .saving:
+ editor(width: geometry.size.width)
+ }
}
- }
- .navigationBarTitleView(
- title: String(localized: "Customize Course", bundle: .core),
- subtitle: viewModel.courseName
- )
- .navBarItems(
- leading: {
- Button(action: cancelTapped) {
- Text("Cancel", bundle: .core).fontWeight(.regular)
+ .navigationTitle(.init("Customize Course", bundle: .core))
+ .optionalNavigationSubtitle(viewModel.courseName)
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ Button(action: cancelTapped) {
+ Text("Cancel", bundle: .core)
+ .fontWeight(.regular)
+ }
}
- },
- trailing: {
- Button(action: doneTapped) {
- Text("Done", bundle: .core).bold()
+
+ ToolbarItem(placement: .topBarTrailing) {
+ Button(action: doneTapped) {
+ Text("Done", bundle: .core)
+ .bold()
+ }
+ .disabled(viewModel.state != .ready)
}
- .disabled(viewModel.state != .ready)
}
- )
- .navigationBarStyle(.modal)
- .onAppear(perform: viewModel.viewDidAppear)
- .alert(isPresented: $viewModel.showError) {
- Alert(title: Text(viewModel.errorText ?? String(localized: "Something went wrong", bundle: .core)))
+ .onAppear(perform: viewModel.viewDidAppear)
+ .alert(isPresented: $viewModel.showError) {
+ Alert(title: Text(viewModel.errorText ?? String(localized: "Something went wrong", bundle: .core)))
+ }
+ } else {
+ GeometryReader { geometry in
+ switch viewModel.state {
+ case .loading:
+ ProgressView()
+ .progressViewStyle(.indeterminateCircle())
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ case .ready, .saving:
+ legacyEditor(width: geometry.size.width)
+ }
+ }
+ .navigationBarTitleView(
+ title: String(localized: "Customize Course", bundle: .core),
+ subtitle: viewModel.courseName
+ )
+ .navBarItems(
+ leading: {
+ Button(action: cancelTapped) {
+ Text("Cancel", bundle: .core).fontWeight(.regular)
+ }
+ },
+ trailing: {
+ Button(action: doneTapped) {
+ Text("Done", bundle: .core).bold()
+ }
+ .disabled(viewModel.state != .ready)
+ }
+ )
+ .navigationBarStyle(.modal)
+ .onAppear(perform: viewModel.viewDidAppear)
+ .alert(isPresented: $viewModel.showError) {
+ Alert(title: Text(viewModel.errorText ?? String(localized: "Something went wrong", bundle: .core)))
+ }
}
}
+ @available(iOS, introduced: 26, message: "Legacy version exists")
private func editor(width: CGFloat) -> some View {
+ EditorForm(isSpinning: viewModel.state == .saving) {
+ let height: CGFloat = 235
+ ZStack {
+ Color(viewModel.courseColor ?? .textDark).frame(width: width, height: height)
+ if let url = viewModel.imageURL {
+ RemoteImage(url, width: width, height: height, shouldHandleAnimatedGif: true)
+ .opacity(viewModel.hideColorOverlay == true ? 1 : 0.4)
+ .accessibility(hidden: true)
+ }
+ }
+ .frame(height: height)
+ .clipped()
+
+ nameRow
+ Divider()
+ defaultViewButtonRow
+ Divider()
+ }
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private func legacyEditor(width: CGFloat) -> some View {
EditorForm(isSpinning: viewModel.state == .saving) {
let height: CGFloat = 235
ZStack {
diff --git a/Core/Core/Features/Courses/CourseDetails/View/LegacyCourseDetailsHeaderView.swift b/Core/Core/Features/Courses/CourseDetails/View/LegacyCourseDetailsHeaderView.swift
new file mode 100644
index 0000000000..ab9776e909
--- /dev/null
+++ b/Core/Core/Features/Courses/CourseDetails/View/LegacyCourseDetailsHeaderView.swift
@@ -0,0 +1,81 @@
+//
+// This file is part of Canvas.
+// Copyright (C) 2022-present Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+//
+
+import SwiftUI
+
+@available(iOS, deprecated: 26, message: "Non-legacy version exists")
+struct LegacyCourseDetailsHeaderView: View {
+ @ObservedObject private var viewModel: LegacyCourseDetailsHeaderViewModel
+ private let width: CGFloat
+
+ public init(viewModel: LegacyCourseDetailsHeaderViewModel, width: CGFloat) {
+ self.viewModel = viewModel
+ self.width = width
+ }
+
+ public var body: some View {
+ ZStack {
+ Color(viewModel.courseColor)
+ .frame(width: width, height: viewModel.height)
+ if let url = viewModel.imageURL {
+ RemoteImage(url, width: width, height: viewModel.height, shouldHandleAnimatedGif: true)
+ .opacity(viewModel.imageOpacity)
+ .accessibility(hidden: true)
+ }
+ VStack(spacing: 3) {
+ Text(viewModel.courseName)
+ .font(.semibold23)
+ .accessibility(identifier: "course-details.title-lbl")
+ .shadow(
+ color: viewModel.courseTitleShadow.color,
+ radius: viewModel.courseTitleShadow.radius
+ )
+ Text(viewModel.termName)
+ .font(.semibold14)
+ .accessibility(identifier: "course-details.subtitle-lbl")
+ .shadow(
+ color: viewModel.courseTitleShadow.color,
+ radius: viewModel.courseTitleShadow.radius
+ )
+ }
+ .padding()
+ .multilineTextAlignment(.center)
+ .foregroundColor(.textLightest)
+ .opacity(viewModel.titleOpacity)
+ }
+ .frame(height: viewModel.height)
+ .clipped()
+ .offset(x: 0, y: viewModel.verticalOffset)
+ }
+}
+
+#if DEBUG
+
+struct LegacyCourseDetailsHeaderView_Previews: PreviewProvider {
+ private static let env = AppEnvironment.shared
+ private static let context = env.globalDatabase.viewContext
+
+ static var previews: some View {
+ let course = Course.save(.make(term: .make()), in: context)
+ let viewModel = LegacyCourseDetailsHeaderViewModel()
+ viewModel.courseUpdated(course)
+ return LegacyCourseDetailsHeaderView(viewModel: viewModel, width: 400)
+ }
+}
+
+#endif
diff --git a/Core/Core/Features/Courses/CourseDetails/ViewModel/CourseDetailsHeaderViewModel.swift b/Core/Core/Features/Courses/CourseDetails/ViewModel/CourseDetailsHeaderViewModel.swift
index 4609cc6d88..762c8442d2 100644
--- a/Core/Core/Features/Courses/CourseDetails/ViewModel/CourseDetailsHeaderViewModel.swift
+++ b/Core/Core/Features/Courses/CourseDetails/ViewModel/CourseDetailsHeaderViewModel.swift
@@ -19,6 +19,7 @@
import Foundation
import SwiftUI
+// MARK: Legacy version exists
public class CourseDetailsHeaderViewModel: ObservableObject {
@Published public private(set) var hideColorOverlay: Bool = false
@Published public private(set) var verticalOffset: CGFloat = 0
@@ -42,10 +43,6 @@ public class CourseDetailsHeaderViewModel: ObservableObject {
self?.hideColorOverlay = self?.settings.first?.hideDashcardColorOverlays == true
}
- private var shouldShow: Bool = false
- private var checkedWidth: CGFloat = .nan
- private var keyboard = KeyboardObserved()
-
public func viewDidAppear() {
settings.refresh()
}
@@ -55,38 +52,6 @@ public class CourseDetailsHeaderViewModel: ObservableObject {
imageURL = course.imageDownloadURL
termName = course.termName ?? ""
courseColor = course.color
- }
-
- public func scrollPositionChanged(_ bounds: ViewBoundsKey.Value) {
- guard let frame = bounds.first?.bounds else { return }
- scrollPositionYChanged(to: frame.minY)
- }
-
- public func shouldShowHeader(in availableSize: CGSize) -> Bool {
- let isRotating = checkedWidth.isFinite && checkedWidth != availableSize.width
- guard isRotating || keyboard.isHiding else { return shouldShow }
-
- shouldShow = self.height < availableSize.height / 2
- checkedWidth = availableSize.width
- return shouldShow
- }
-
- public var visibleHeight: CGFloat {
- shouldShow ? height : 0
- }
-
- private func scrollPositionYChanged(to value: CGFloat) {
- if value <= 0 { // scrolling down to content
- verticalOffset = min(0, value / 2)
-
- // Starts from 0 and reaches 1 when the image is fully pushed out of screen
- let offsetRatio = abs(verticalOffset) / (height / 2)
- imageOpacity = hideColorOverlay ? 1 : (1 - offsetRatio) * Self.originalImageOpacity
- titleOpacity = 1 - offsetRatio
- } else { // pull to refresh gesture, we allow the image to move along with the content
- verticalOffset = value
- imageOpacity = hideColorOverlay ? 1 : Self.originalImageOpacity
- titleOpacity = 1
- }
+ imageOpacity = hideColorOverlay ? 1 : Self.originalImageOpacity
}
}
diff --git a/Core/Core/Features/Courses/CourseDetails/ViewModel/CourseDetailsViewModel.swift b/Core/Core/Features/Courses/CourseDetails/ViewModel/CourseDetailsViewModel.swift
index 5fc3220f88..4fa3ec5f4c 100644
--- a/Core/Core/Features/Courses/CourseDetails/ViewModel/CourseDetailsViewModel.swift
+++ b/Core/Core/Features/Courses/CourseDetails/ViewModel/CourseDetailsViewModel.swift
@@ -36,6 +36,9 @@ public class CourseDetailsViewModel: ObservableObject {
@Published public private(set) var homeRoute: URL?
@Published public private(set) var showHome: Bool
+ @available(iOS, deprecated: 26)
+ public let legacyHeaderViewModel = LegacyCourseDetailsHeaderViewModel()
+
public var showSettings: Bool { isTeacher }
public var showStudentView: Bool { isTeacher }
public var courseName: String { course.first?.name ?? "" }
@@ -94,6 +97,7 @@ public class CourseDetailsViewModel: ObservableObject {
public func viewDidAppear() {
selectionViewModel.viewDidAppear()
headerViewModel.viewDidAppear()
+ legacyHeaderViewModel.viewDidAppear()
requestAttendanceTool()
permissions.refresh()
customGradeStatuses.refresh()
@@ -160,6 +164,7 @@ public class CourseDetailsViewModel: ObservableObject {
private func courseDidUpdate() {
guard let course = course.first else { return }
headerViewModel.courseUpdated(course)
+ legacyHeaderViewModel.courseUpdated(course)
courseColor = course.color
setupHome(course: course)
tabs.exhaust()
diff --git a/Core/Core/Features/Courses/CourseDetails/ViewModel/LegacyCourseDetailsHeaderViewModel.swift b/Core/Core/Features/Courses/CourseDetails/ViewModel/LegacyCourseDetailsHeaderViewModel.swift
new file mode 100644
index 0000000000..32d50e015b
--- /dev/null
+++ b/Core/Core/Features/Courses/CourseDetails/ViewModel/LegacyCourseDetailsHeaderViewModel.swift
@@ -0,0 +1,93 @@
+//
+// This file is part of Canvas.
+// Copyright (C) 2025-present Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+//
+
+import Foundation
+import SwiftUI
+
+@available(iOS, deprecated: 26, message: "Non-legacy version exists")
+public class LegacyCourseDetailsHeaderViewModel: ObservableObject {
+ @Published public private(set) var hideColorOverlay: Bool = false
+ @Published public private(set) var verticalOffset: CGFloat = 0
+ @Published public private(set) var imageOpacity: CGFloat = originalImageOpacity
+ @Published public private(set) var titleOpacity: CGFloat = 1
+ @Published public private(set) var courseName = ""
+ @Published public private(set) var courseColor: UIColor = .clear
+ @Published public private(set) var termName = ""
+ @Published public private(set) var imageURL: URL?
+
+ private static let originalImageOpacity: CGFloat = 0.16
+
+ public let courseTitleShadow = (
+ color: Color(UIColor.backgroundDarkest.withAlphaComponent(0.80)),
+ radius: 4 as CGFloat
+ )
+ public let height: CGFloat = 235
+
+ private let env = AppEnvironment.shared
+ private lazy var settings: Store = env.subscribe(GetUserSettings(userID: "self")) { [weak self] in
+ self?.hideColorOverlay = self?.settings.first?.hideDashcardColorOverlays == true
+ }
+
+ private var shouldShow: Bool = false
+ private var checkedWidth: CGFloat = .nan
+ private var keyboard = KeyboardObserved()
+
+ public func viewDidAppear() {
+ settings.refresh()
+ }
+
+ public func courseUpdated(_ course: Course) {
+ courseName = course.name ?? ""
+ imageURL = course.imageDownloadURL
+ termName = course.termName ?? ""
+ courseColor = course.color
+ }
+
+ public func scrollPositionChanged(_ bounds: ViewBoundsKey.Value) {
+ guard let frame = bounds.first?.bounds else { return }
+ scrollPositionYChanged(to: frame.minY)
+ }
+
+ public func shouldShowHeader(in availableSize: CGSize) -> Bool {
+ let isRotating = checkedWidth.isFinite && checkedWidth != availableSize.width
+ guard isRotating || keyboard.isHiding else { return shouldShow }
+
+ shouldShow = self.height < availableSize.height / 2
+ checkedWidth = availableSize.width
+ return shouldShow
+ }
+
+ public var visibleHeight: CGFloat {
+ shouldShow ? height : 0
+ }
+
+ private func scrollPositionYChanged(to value: CGFloat) {
+ if value <= 0 { // scrolling down to content
+ verticalOffset = min(0, value / 2)
+
+ // Starts from 0 and reaches 1 when the image is fully pushed out of screen
+ let offsetRatio = abs(verticalOffset) / (height / 2)
+ imageOpacity = hideColorOverlay ? 1 : (1 - offsetRatio) * Self.originalImageOpacity
+ titleOpacity = 1 - offsetRatio
+ } else { // pull to refresh gesture, we allow the image to move along with the content
+ verticalOffset = value
+ imageOpacity = hideColorOverlay ? 1 : Self.originalImageOpacity
+ titleOpacity = 1
+ }
+ }
+}
diff --git a/Core/Core/Features/Dashboard/Container/View/DashboardContainerView.swift b/Core/Core/Features/Dashboard/Container/View/DashboardContainerView.swift
index 6fb5899863..08b9e15eda 100644
--- a/Core/Core/Features/Dashboard/Container/View/DashboardContainerView.swift
+++ b/Core/Core/Features/Dashboard/Container/View/DashboardContainerView.swift
@@ -77,7 +77,23 @@ public struct DashboardContainerView: View, ScreenViewTrackable {
}
.background(Color.backgroundLightest.edgesIgnoringSafeArea(.all))
.navigationBarDashboard()
- .navigationBarItems(leading: profileMenuButton, trailing: rightNavBarButtons)
+ .toolbar {
+ if #available(iOS 26, *) {
+ ToolbarItem(placement: .topBarLeading) {
+ profileMenuButton
+ }
+ ToolbarItem(placement: .topBarTrailing) {
+ rightNavBarButtons
+ }
+ } else {
+ ToolbarItem(placement: .topBarLeading) {
+ legacyProfileMenuButton
+ }
+ ToolbarItem(placement: .topBarTrailing) {
+ legacyRightNavBarButtons
+ }
+ }
+ }
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
fileUploadNotificationCardViewModel.sceneDidBecomeActive.send()
}
@@ -119,7 +135,19 @@ public struct DashboardContainerView: View, ScreenViewTrackable {
// MARK: - Nav Bar Buttons
- private var profileMenuButton: some View {
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ private var profileMenuButton: some View {
+ Button {
+ env.router.route(to: "/profile", from: controller, options: .modal())
+ } label: {
+ Image.hamburgerSolid
+ }
+ .identifier("Dashboard.profileButton")
+ .accessibility(label: Text("Profile Menu, Closed", bundle: .core, comment: "Accessibility text describing the Profile Menu button and its state"))
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private var legacyProfileMenuButton: some View {
Button {
env.router.route(to: "/profile", from: controller, options: .modal())
} label: {
@@ -131,19 +159,58 @@ public struct DashboardContainerView: View, ScreenViewTrackable {
.accessibility(label: Text("Profile Menu, Closed", bundle: .core, comment: "Accessibility text describing the Profile Menu button and its state"))
}
+ @ViewBuilder
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ private var rightNavBarButtons: some View {
+ if courseCardListViewModel.shouldShowSettingsButton {
+ if offlineModeViewModel.isOfflineFeatureEnabled, env.app == .student {
+ optionsKebabMenu
+ } else {
+ dashboardSettingsButton
+ }
+ }
+ }
+
@ViewBuilder
- private var rightNavBarButtons: some View {
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private var legacyRightNavBarButtons: some View {
if courseCardListViewModel.shouldShowSettingsButton {
if offlineModeViewModel.isOfflineFeatureEnabled, env.app == .student {
- optionsKebabButton
+ legacyOptionsKebabButton
} else {
- dashboardSettingsButton
+ legacyDashboardSettingsButton
}
}
}
+ @ViewBuilder
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ private var optionsKebabMenu: some View {
+ Menu {
+ Button(.init("Manage Offline Content", bundle: .core)) {
+ if offlineModeViewModel.isOffline {
+ UIAlertController.showItemNotAvailableInOfflineAlert()
+ } else {
+ env.router.route(to: "/offline/sync_picker", from: controller, options: .modal(isDismissable: false, embedInNav: true))
+ }
+ }
+
+ Button(.init("Dashboard Settings", bundle: .core)) {
+ guard controller.value.presentedViewController == nil else {
+ controller.value.presentedViewController?.dismiss(animated: true)
+ return
+ }
+ viewModel.settingsButtonTapped.send()
+ }
+ } label: {
+ Image.moreSolid
+ }
+ .accessibilityLabel(Text("Dashboard Options", bundle: .core))
+ }
+
@ViewBuilder
- private var optionsKebabButton: some View {
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private var legacyOptionsKebabButton: some View {
Button {
// Dismiss dashboard settings popover
guard controller.value.presentedViewController == nil else {
@@ -180,8 +247,26 @@ public struct DashboardContainerView: View, ScreenViewTrackable {
}
}
+ @ViewBuilder
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ private var dashboardSettingsButton: some View {
+ Button {
+ guard controller.value.presentedViewController == nil else {
+ controller.value.presentedViewController?.dismiss(animated: true)
+ return
+ }
+
+ viewModel.settingsButtonTapped.send()
+ } label: {
+ Image.settingsSolid
+ }
+ .accessibilityLabel(Text("Dashboard settings", bundle: .core))
+ .accessibilityIdentifier("Dashboard.settingsButton")
+ }
+
@ViewBuilder
- private var dashboardSettingsButton: some View {
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private var legacyDashboardSettingsButton: some View {
Button {
guard controller.value.presentedViewController == nil else {
controller.value.presentedViewController?.dismiss(animated: true)
diff --git a/Core/Core/Features/Dashboard/Container/View/DashboardNavigationBar.swift b/Core/Core/Features/Dashboard/Container/View/DashboardNavigationBar.swift
index 1a7517a29c..1805158a12 100644
--- a/Core/Core/Features/Dashboard/Container/View/DashboardNavigationBar.swift
+++ b/Core/Core/Features/Dashboard/Container/View/DashboardNavigationBar.swift
@@ -27,13 +27,23 @@ struct DashboardNavigationBar: ViewModifier {
@State private var isElevatedTabBar = false
func body(content: Content) -> some View {
- if #available(iOS 18.0, *) {
+ if #available(iOS 26, *) {
+ content
+ .toolbar {
+ ToolbarItem(placement: .principal) {
+ navBarLogo
+ }
+ }
+ .onChange(of: horizontalSizeClass, initial: true) { _, newValue in
+ updateNavBarLogoVisibility(horizontalSizeClass: newValue)
+ }
+ } else if #available(iOS 18.0, *) {
content
.toolbarBackgroundVisibility(.visible, for: .navigationBar)
.toolbarBackground(Color(uiColor: Brand.shared.navBackground), for: .navigationBar)
.toolbar {
ToolbarItem(placement: .principal) {
- navBarLogo
+ legacyNavBarLogo
}
}
.onChange(of: horizontalSizeClass, initial: true) { _, newValue in
@@ -50,8 +60,27 @@ struct DashboardNavigationBar: ViewModifier {
isElevatedTabBar = horizontalSizeClass == .regular
}
+ @ViewBuilder
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ fileprivate var navBarLogo: some View {
+ if !isElevatedTabBar, let headerImage = Brand.shared.headerImage {
+ ZStack {
+ Brand.shared.headerImageBackground.asColor
+
+ Image(uiImage: headerImage)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .accessibilityHidden(true)
+ }
+ .clipShape(RoundedRectangle(cornerRadius: 8))
+ .frame(width: 44, height: 44)
+ .shadow(radius: 8)
+ }
+ }
+
@ViewBuilder
- private var navBarLogo: some View {
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ fileprivate var legacyNavBarLogo: some View {
if !isElevatedTabBar, let headerImage = Brand.shared.headerImage {
ZStack {
Color(Brand.shared.headerImageBackground)
diff --git a/Core/Core/Features/Dashboard/CourseCardList/View/CustomizeCourseView.swift b/Core/Core/Features/Dashboard/CourseCardList/View/CustomizeCourseView.swift
index 549e26e9ce..ccb1685c5e 100644
--- a/Core/Core/Features/Dashboard/CourseCardList/View/CustomizeCourseView.swift
+++ b/Core/Core/Features/Dashboard/CourseCardList/View/CustomizeCourseView.swift
@@ -134,15 +134,17 @@ struct CustomizeCourseView: View {
#if DEBUG
#Preview {
- CustomizeCourseView(
- viewModel: CustomizeCourseViewModel(
- courseId: "1",
- courseImage: nil,
- courseColor: .course1,
- courseName: "Test Course",
- hideColorOverlay: false
+ NavigationStack {
+ CustomizeCourseView(
+ viewModel: CustomizeCourseViewModel(
+ courseId: "1",
+ courseImage: nil,
+ courseColor: .course1,
+ courseName: "Test Course",
+ hideColorOverlay: false
+ )
)
- )
+ }
}
#endif
diff --git a/Core/Core/Features/Dashboard/K5/View/K5DashboardView.swift b/Core/Core/Features/Dashboard/K5/View/K5DashboardView.swift
index 3100029bd1..ae3af0a954 100644
--- a/Core/Core/Features/Dashboard/K5/View/K5DashboardView.swift
+++ b/Core/Core/Features/Dashboard/K5/View/K5DashboardView.swift
@@ -48,7 +48,7 @@ public struct K5DashboardView: View {
viewModel.profileButtonPressed(router: env.router, viewController: controller)
}, label: {
Image.hamburgerSolid
- .foregroundColor(Color(Brand.shared.navTextColor))
+ .foregroundStyleBelow26(Brand.shared.navTextColor.asColor)
})
.identifier("Dashboard.profileButton")
.accessibility(label: Text("Profile Menu, Closed", bundle: .core, comment: "Accessibility text describing the Profile Menu button and its state"))
diff --git a/Core/Core/Features/Discussions/AnnouncementList/AnnouncementListViewController.swift b/Core/Core/Features/Discussions/AnnouncementList/AnnouncementListViewController.swift
index 5bbdc47ebf..14e891145a 100644
--- a/Core/Core/Features/Discussions/AnnouncementList/AnnouncementListViewController.swift
+++ b/Core/Core/Features/Discussions/AnnouncementList/AnnouncementListViewController.swift
@@ -60,7 +60,12 @@ public class AnnouncementListViewController: ScreenViewTrackableViewController,
public override func viewDidLoad() {
super.viewDidLoad()
- setupTitleViewInNavbar(title: String(localized: "Announcements", bundle: .core))
+
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Announcements", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Announcements", bundle: .core))
+ }
addButton.accessibilityLabel = String(localized: "Create Announcement", bundle: .core)
@@ -110,12 +115,18 @@ public class AnnouncementListViewController: ScreenViewTrackableViewController,
}
func updateNavBar() {
- if colors.pending == false,
- let name = course?.first?.name ?? group?.first?.name,
- let color = course?.first?.color ?? group?.first?.color {
- updateNavBar(subtitle: name, color: color)
- view.tintColor = color
- }
+ if #available(iOS 26, *) {
+ if let name = course?.first?.name ?? group?.first?.name {
+ navigationItem.subtitle = name
+ }
+ } else {
+ if colors.pending == false,
+ let name = course?.first?.name ?? group?.first?.name,
+ let color = course?.first?.color ?? group?.first?.color {
+ updateNavBar(subtitle: name, color: color)
+ view.tintColor = color
+ }
+ }
let canAdd = (course?.first?.canCreateAnnouncement ?? group?.first?.canCreateAnnouncement) == true
navigationItem.rightBarButtonItem = canAdd ? addButton : nil
}
diff --git a/Core/Core/Features/Discussions/DiscussionDetails/DiscussionDetailsViewController.swift b/Core/Core/Features/Discussions/DiscussionDetails/DiscussionDetailsViewController.swift
index 6194521392..4cac65f510 100644
--- a/Core/Core/Features/Discussions/DiscussionDetails/DiscussionDetailsViewController.swift
+++ b/Core/Core/Features/Discussions/DiscussionDetails/DiscussionDetailsViewController.swift
@@ -124,10 +124,18 @@ public class DiscussionDetailsViewController: ScreenViewTrackableViewController,
public override func viewDidLoad() {
super.viewDidLoad()
- setupTitleViewInNavbar(title: isAnnouncement
- ? String(localized: "Announcement Details", bundle: .core)
- : String(localized: "Discussion Details", bundle: .core)
- )
+
+ if #available(iOS 26, *) {
+ navigationItem.title = isAnnouncement
+ ? String(localized: "Announcement Details", bundle: .core)
+ : String(localized: "Discussion Details", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: isAnnouncement
+ ? String(localized: "Announcement Details", bundle: .core)
+ : String(localized: "Discussion Details", bundle: .core)
+ )
+ }
+
courseSectionsView.isHidden = true
optionsButton.accessibilityLabel = String(localized: "Options", bundle: .core)
@@ -162,9 +170,15 @@ public class DiscussionDetailsViewController: ScreenViewTrackableViewController,
webView.handle("ready") { [weak self] _ in self?.ready() }
if showRepliesToEntryID != nil {
- titleSubtitleView.title = isAnnouncement
- ? String(localized: "Announcement Replies", bundle: .core)
- : String(localized: "Discussion Replies", bundle: .core)
+ if #available(iOS 26, *) {
+ navigationItem.title = isAnnouncement
+ ? String(localized: "Announcement Replies", bundle: .core)
+ : String(localized: "Discussion Replies", bundle: .core)
+ } else {
+ titleSubtitleView.title = isAnnouncement
+ ? String(localized: "Announcement Replies", bundle: .core)
+ : String(localized: "Discussion Replies", bundle: .core)
+ }
navigationItem.rightBarButtonItem = nil
}
@@ -235,16 +249,35 @@ public class DiscussionDetailsViewController: ScreenViewTrackableViewController,
}
spinnerView.color = color
refreshControl.color = color
- titleSubtitleView.title = showRepliesToEntryID != nil ? (
- isAnnouncement
- ? String(localized: "Announcement Replies", bundle: .core)
- : String(localized: "Discussion Replies", bundle: .core)
- ) : (
- isAnnouncement
- ? String(localized: "Announcement Details", bundle: .core)
- : String(localized: "Discussion Details", bundle: .core)
- )
- updateNavBar(subtitle: name, color: color)
+ if #available(iOS 26, *) {
+ navigationItem.title = showRepliesToEntryID != nil ? (
+ isAnnouncement
+ ? String(localized: "Announcement Replies", bundle: .core)
+ : String(localized: "Discussion Replies", bundle: .core)
+ ) : (
+ isAnnouncement
+ ? String(localized: "Announcement Details", bundle: .core)
+ : String(localized: "Discussion Details", bundle: .core)
+ )
+ } else {
+ titleSubtitleView.title = showRepliesToEntryID != nil ? (
+ isAnnouncement
+ ? String(localized: "Announcement Replies", bundle: .core)
+ : String(localized: "Discussion Replies", bundle: .core)
+ ) : (
+ isAnnouncement
+ ? String(localized: "Announcement Details", bundle: .core)
+ : String(localized: "Discussion Details", bundle: .core)
+ )
+ }
+
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = name
+ // WebView needs this color
+ self.color = color
+ } else {
+ updateNavBar(subtitle: name, color: color)
+ }
}
func update() {
diff --git a/Core/Core/Features/Discussions/DiscussionEditor/ViewModel/DiscussionCreateWebViewModel.swift b/Core/Core/Features/Discussions/DiscussionEditor/ViewModel/DiscussionCreateWebViewModel.swift
index 8ca059e326..2a9fbd1472 100644
--- a/Core/Core/Features/Discussions/DiscussionEditor/ViewModel/DiscussionCreateWebViewModel.swift
+++ b/Core/Core/Features/Discussions/DiscussionEditor/ViewModel/DiscussionCreateWebViewModel.swift
@@ -46,8 +46,14 @@ public struct DiscussionCreateWebViewModel: EmbeddedWebPageViewModel {
}
public func leadingNavigationButton(host: UIViewController) -> InstUI.NavigationBarButton? {
- .cancel(isBackgroundContextColor: true) { [router] in
- router.dismiss(host)
+ if #available(iOS 26, *) {
+ .cancel { [router] in
+ router.dismiss(host)
+ }
+ } else {
+ .cancel(isBackgroundContextColor: true) { [router] in
+ router.dismiss(host)
+ }
}
}
diff --git a/Core/Core/Features/Discussions/DiscussionList/DiscussionListViewController.swift b/Core/Core/Features/Discussions/DiscussionList/DiscussionListViewController.swift
index 939857760b..cf27858d86 100644
--- a/Core/Core/Features/Discussions/DiscussionList/DiscussionListViewController.swift
+++ b/Core/Core/Features/Discussions/DiscussionList/DiscussionListViewController.swift
@@ -73,7 +73,12 @@ public class DiscussionListViewController: ScreenViewTrackableViewController, Co
public override func viewDidLoad() {
super.viewDidLoad()
- setupTitleViewInNavbar(title: String(localized: "Discussions", bundle: .core))
+
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Discussions", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Discussions", bundle: .core))
+ }
addButton.accessibilityLabel = String(localized: "Create Discussion", bundle: .core)
addButton.accessibilityIdentifier = "DiscussionList.newButton"
@@ -121,7 +126,11 @@ public class DiscussionListViewController: ScreenViewTrackableViewController, Co
if colors.pending == false,
let name = course?.first?.name ?? group?.first?.name,
let color = course?.first?.color ?? group?.first?.color {
- updateNavBar(subtitle: name, color: color)
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = name
+ } else {
+ updateNavBar(subtitle: name, color: color)
+ }
view.tintColor = color
}
let canAdd = (course?.first?.canCreateDiscussionTopic ?? group?.first?.canCreateDiscussionTopic) == true
@@ -207,16 +216,29 @@ extension DiscussionListViewController: UITableViewDataSource, UITableViewDelega
}
public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
- switch topics.sections?[section].name {
- case "0":
- return SectionHeaderView.create(title: String(localized: "Pinned Discussions", bundle: .core), section: section)
- case "1":
- return SectionHeaderView.create(title: String(localized: "Discussions", bundle: .core), section: section)
- case "2":
- return SectionHeaderView.create(title: String(localized: "Closed for Comments", bundle: .core), section: section)
- default:
- return nil
- }
+ if #available(iOS 26, *) {
+ switch topics.sections?[section].name {
+ case "0":
+ return SectionHeaderView.create(title: String(localized: "Pinned Discussions", bundle: .core), section: section)
+ case "1":
+ return SectionHeaderView.create(title: String(localized: "Discussions", bundle: .core), section: section)
+ case "2":
+ return SectionHeaderView.create(title: String(localized: "Closed for Comments", bundle: .core), section: section)
+ default:
+ return nil
+ }
+ } else {
+ switch topics.sections?[section].name {
+ case "0":
+ return LegacySectionHeaderView.create(title: String(localized: "Pinned Discussions", bundle: .core), section: section)
+ case "1":
+ return LegacySectionHeaderView.create(title: String(localized: "Discussions", bundle: .core), section: section)
+ case "2":
+ return LegacySectionHeaderView.create(title: String(localized: "Closed for Comments", bundle: .core), section: section)
+ default:
+ return nil
+ }
+ }
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
diff --git a/Core/Core/Features/Files/View/FileList/FileListViewController.swift b/Core/Core/Features/Files/View/FileList/FileListViewController.swift
index 5645409648..e761fcd964 100644
--- a/Core/Core/Features/Files/View/FileList/FileListViewController.swift
+++ b/Core/Core/Features/Files/View/FileList/FileListViewController.swift
@@ -32,7 +32,27 @@ public class FileListViewController: ScreenViewTrackableViewController, ColoredN
@IBOutlet weak var loadingView: CircleProgressView!
@IBOutlet weak var tableView: UITableView!
- lazy var addButton = UIBarButtonItem(image: .addSolid, style: .plain, target: self, action: #selector(addItem))
+ // Legacy version exists, cannot be marked unavailable
+ lazy var addButton: UIBarButtonItem = {
+ var button = UIBarButtonItem(image: .addSolid)
+
+ let addFolderAction = UIAction(title: .init(localized: "Add Folder", bundle: .core), image: .folderLine) { [weak self] _ in
+ self?.addFolder()
+ }
+ addFolderAction.accessibilityIdentifier = "FileList.addFolderButton"
+
+ let addFileAction = UIAction(title: .init(localized: "Add File", bundle: .core), image: .addDocumentLine) { [weak self] _ in
+ guard let self = self else { return }
+ self.filePicker.pick(from: self)
+ }
+ addFileAction.accessibilityIdentifier = "FileList.addFileButton"
+
+ let menu = UIMenu(children: !isStudentAccessRestricted ? [addFolderAction, addFileAction] : [addFolderAction])
+ return UIBarButtonItem(image: .addSolid, menu: menu)
+ }()
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ lazy var legacyAddButton = UIBarButtonItem(image: .addSolid, style: .plain, target: self, action: #selector(addItem))
lazy var editButton = UIBarButtonItem(
title: String(localized: "Edit", bundle: .core), style: .plain,
target: self, action: #selector(edit)
@@ -103,10 +123,17 @@ public class FileListViewController: ScreenViewTrackableViewController, ColoredN
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .backgroundLightest
- setupTitleViewInNavbar(title: String(localized: "Files", bundle: .core))
+
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Files", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Files", bundle: .core))
+ }
addButton.accessibilityIdentifier = "FileList.addButton"
addButton.accessibilityLabel = String(localized: "Add Item", bundle: .core)
+ legacyAddButton.accessibilityIdentifier = "FileList.addButton"
+ legacyAddButton.accessibilityLabel = String(localized: "Add Item", bundle: .core)
editButton.accessibilityIdentifier = "FileList.editButton"
emptyImageView.image = UIImage(named: Panda.FilePicker.name, in: .core, compatibleWith: nil)
@@ -146,21 +173,32 @@ public class FileListViewController: ScreenViewTrackableViewController, ColoredN
super.viewWillAppear(animated)
keyboard = KeyboardTransitioning(view: view, space: keyboardSpace)
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
- if context.contextType == .user {
- navigationController?.navigationBar.useGlobalNavStyle()
- } else {
- navigationController?.navigationBar.useContextColor(color)
- }
+
+ if #unavailable(iOS 26) {
+ if context.contextType == .user {
+ navigationController?.navigationBar.useGlobalNavStyle()
+ } else {
+ navigationController?.navigationBar.useContextColor(color)
+ }
+ }
}
func updateNavBar() {
- if let course = course?.first {
- updateNavBar(subtitle: course.name, color: course.color)
- } else if let group = group?.first {
- updateNavBar(subtitle: group.name, color: group.color)
- } else if context.contextType == .user {
- color = .textDark
- }
+ if #available(iOS 26, *) {
+ if let course = course?.first {
+ navigationItem.subtitle = course.name
+ } else if let group = group?.first {
+ navigationItem.subtitle = group.name
+ }
+ } else {
+ if let course = course?.first {
+ updateNavBar(subtitle: course.name, color: course.color)
+ } else if let group = group?.first {
+ updateNavBar(subtitle: group.name, color: group.color)
+ } else if context.contextType == .user {
+ color = .textDark
+ }
+ }
view.tintColor = color
updateNavButtons()
}
@@ -183,7 +221,13 @@ public class FileListViewController: ScreenViewTrackableViewController, ColoredN
loadingView.isHidden = !folder.pending || !folder.isEmpty || folder.error != nil || refreshControl.isRefreshing
errorView.isHidden = folder.error == nil
let title = (path.isEmpty ? nil : folder.first?.name) ?? String(localized: "Files", bundle: .core)
- setupTitleViewInNavbar(title: title)
+
+ if #available(iOS 26, *) {
+ navigationItem.title = title
+ } else {
+ setupTitleViewInNavbar(title: title)
+ }
+
updateNavButtons()
guard let folder = folder.first, items == nil else { return update() }
@@ -310,8 +354,9 @@ extension FileListViewController: UISearchBarDelegate {
extension FileListViewController: FilePickerDelegate {
func updateNavButtons() {
+ let button = if #available(iOS 26, *) { addButton } else { legacyAddButton }
navigationItem.rightBarButtonItems = [
- canAddItem ? addButton : nil,
+ canAddItem ? button : nil,
canEditFolder ? editButton : nil
].compactMap { $0 }
}
@@ -334,6 +379,7 @@ extension FileListViewController: FilePickerDelegate {
folder.first?.canUpload == true
}
+ @available(iOS, deprecated: 26)
@objc func addItem() {
let sheet = BottomSheetPickerViewController.create()
sheet.addAction(
diff --git a/Core/Core/Features/Grades/View/GradeFilterScreen.swift b/Core/Core/Features/Grades/View/GradeFilterScreen.swift
index b7bf20cb88..e42c99bffa 100644
--- a/Core/Core/Features/Grades/View/GradeFilterScreen.swift
+++ b/Core/Core/Features/Grades/View/GradeFilterScreen.swift
@@ -31,21 +31,44 @@ public struct GradeFilterScreen: View {
// MARK: - Body
public var body: some View {
- ScrollView(showsIndicators: false) {
- VStack(spacing: 0) {
- if viewModel.isShowGradingPeriodsView {
- gradingPeriodSection
- }
- sortBySection
- }
- .navigationBarTitleView(
- title: String(localized: "Grade List Preferences", bundle: .core),
- subtitle: viewModel.courseName
- )
- .navigationBarItems(leading: cancelButton, trailing: sendButton)
- .navigationBarStyle(.modal)
- }
- .background(Color.backgroundLightest)
+ if #available(iOS 26, *) {
+ ScrollView(showsIndicators: false) {
+ VStack(spacing: 0) {
+ if viewModel.isShowGradingPeriodsView {
+ gradingPeriodSection
+ }
+ sortBySection
+ }
+ .navigationTitle(.init("Grade List Preferences", bundle: .core))
+ .optionalNavigationSubtitle(viewModel.courseName)
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ cancelButton
+ }
+
+ ToolbarItem(placement: .topBarTrailing) {
+ sendButton
+ }
+ }
+ }
+ .background(Color.backgroundLightest)
+ } else {
+ ScrollView(showsIndicators: false) {
+ VStack(spacing: 0) {
+ if viewModel.isShowGradingPeriodsView {
+ gradingPeriodSection
+ }
+ sortBySection
+ }
+ .navigationBarTitleView(
+ title: String(localized: "Grade List Preferences", bundle: .core),
+ subtitle: viewModel.courseName
+ )
+ .navigationBarItems(leading: cancelButton, trailing: legacySendButton)
+ .navigationBarStyle(.modal)
+ }
+ .background(Color.backgroundLightest)
+ }
}
private var gradingPeriodSection: some View {
@@ -64,7 +87,23 @@ public struct GradeFilterScreen: View {
)
}
- private var sendButton: some View {
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ @ViewBuilder
+ private var sendButton: some View {
+ if viewModel.saveButtonIsEnabled {
+ Button {
+ viewModel.saveButtonTapped(viewController: viewController)
+ } label: {
+ Text(String(localized: "Done", bundle: .core))
+ .font(.semibold16)
+ }
+ .buttonStyle(.glassProminent)
+ .accessibilityIdentifier("GradeFilter.saveButton")
+ }
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private var legacySendButton: some View {
Button {
viewModel.saveButtonTapped(viewController: viewController)
} label: {
diff --git a/Core/Core/Features/Grades/View/GradeListScreen.swift b/Core/Core/Features/Grades/View/GradeListScreen.swift
index e6a58df9e9..1fa2647356 100644
--- a/Core/Core/Features/Grades/View/GradeListScreen.swift
+++ b/Core/Core/Features/Grades/View/GradeListScreen.swift
@@ -64,51 +64,95 @@ public struct GradeListScreen: View, ScreenViewTrackable {
// MARK: - Components
public var body: some View {
- ZStack {
- ScrollView(showsIndicators: false) {
- contentView
- }
- .background(Color.backgroundLight)
- .accessibilityHidden(isScoreEditorPresented)
- .refreshable {
- await withCheckedContinuation { continuation in
- viewModel.pullToRefreshDidTrigger.accept {
- continuation.resume()
- }
- }
- }
+ if #available(iOS 26, *) {
+ ZStack {
+ ScrollView(showsIndicators: false) {
+ contentView
+ }
+ .background(Color.backgroundLight)
+ .accessibilityHidden(isScoreEditorPresented)
+ .refreshable {
+ await withCheckedContinuation { continuation in
+ viewModel.pullToRefreshDidTrigger.accept {
+ continuation.resume()
+ }
+ }
+ }
- whatIfScoreEditorView
- }
- .animation(.smooth, value: isScoreEditorPresented)
- .safeAreaInset(edge: .top, spacing: 0) {
- switch viewModel.state {
- case .data, .empty: GradeListHeaderView(
- viewModel: viewModel,
- toggleViewIsVisible: toggleViewIsVisible
- )
- default: SwiftUI.EmptyView()
- }
- }
- .background(Color.backgroundLightest)
- .tint(viewModel.courseColor?.asColor)
- .navigationBarTitleView(
- title: String(localized: "Grades", bundle: .core),
- subtitle: viewModel.courseName
- )
- .toolbar {
- RevertWhatIfScoreButton(isWhatIfScoreModeOn: viewModel.isWhatIfScoreModeOn) {
- viewModel.isShowingRevertDialog = true
- }
- ToolbarItem(placement: .primaryAction) {
- GradeListFilterButton(viewModel: viewModel)
- }
- }
- .navigationBarStyle(.color(nil))
- .confirmationAlert(
- isPresented: $viewModel.isShowingRevertDialog,
- presenting: viewModel.confirmRevertAlertViewModel
- )
+ whatIfScoreEditorView
+ }
+ .animation(.smooth, value: isScoreEditorPresented)
+ .safeAreaInset(edge: .top, spacing: 0) {
+ switch viewModel.state {
+ case .data, .empty: GradeListHeaderView(
+ viewModel: viewModel,
+ toggleViewIsVisible: toggleViewIsVisible
+ )
+ default: SwiftUI.EmptyView()
+ }
+ }
+ .background(Color.backgroundLightest)
+ .navigationTitle(.init("Grades", bundle: .core))
+ .optionalNavigationSubtitle(viewModel.courseName)
+ .toolbar {
+ RevertWhatIfScoreButton(isWhatIfScoreModeOn: viewModel.isWhatIfScoreModeOn) {
+ viewModel.isShowingRevertDialog = true
+ }
+ ToolbarItem {
+ GradeListFilterButton(viewModel: viewModel)
+ }
+ }
+ .navigationBarStyle(.color(nil))
+ .confirmationAlert(
+ isPresented: $viewModel.isShowingRevertDialog,
+ presenting: viewModel.confirmRevertAlertViewModel
+ )
+ } else {
+ ZStack {
+ ScrollView(showsIndicators: false) {
+ contentView
+ }
+ .background(Color.backgroundLight)
+ .accessibilityHidden(isScoreEditorPresented)
+ .refreshable {
+ await withCheckedContinuation { continuation in
+ viewModel.pullToRefreshDidTrigger.accept {
+ continuation.resume()
+ }
+ }
+ }
+
+ whatIfScoreEditorView
+ }
+ .animation(.smooth, value: isScoreEditorPresented)
+ .safeAreaInset(edge: .top, spacing: 0) {
+ switch viewModel.state {
+ case .data, .empty: GradeListHeaderView(
+ viewModel: viewModel,
+ toggleViewIsVisible: toggleViewIsVisible
+ )
+ default: SwiftUI.EmptyView()
+ }
+ }
+ .background(Color.backgroundLightest)
+ .navigationBarTitleView(
+ title: String(localized: "Grades", bundle: .core),
+ subtitle: viewModel.courseName
+ )
+ .toolbar {
+ RevertWhatIfScoreButton(isWhatIfScoreModeOn: viewModel.isWhatIfScoreModeOn) {
+ viewModel.isShowingRevertDialog = true
+ }
+ ToolbarItem(placement: .primaryAction) {
+ LegacyGradeListFilterButton(viewModel: viewModel)
+ }
+ }
+ .navigationBarStyle(.color(nil))
+ .confirmationAlert(
+ isPresented: $viewModel.isShowingRevertDialog,
+ presenting: viewModel.confirmRevertAlertViewModel
+ )
+ }
}
@ViewBuilder
@@ -227,14 +271,14 @@ private struct RevertWhatIfScoreButton: ToolbarContent {
let buttonDidTap: () -> Void
var body: some ToolbarContent {
- ToolbarItemGroup(placement: .navigationBarTrailing) {
+ ToolbarItem(placement: .navigationBarTrailing) {
if isWhatIfScoreModeOn {
Button(action: {
buttonDidTap()
}) {
Image(uiImage: .replyLine)
.resizable()
- .foregroundColor(.textLightest.variantForLightMode)
+ .foregroundStyleBelow26(.textLightest.variantForLightMode)
}
.frame(alignment: .leading)
.accessibilityLabel(Text("Revert", bundle: .core))
diff --git a/Core/Core/Features/Grades/View/Header/GradeListFilterButton.swift b/Core/Core/Features/Grades/View/Header/GradeListFilterButton.swift
index 64913f2f7e..79bb134865 100644
--- a/Core/Core/Features/Grades/View/Header/GradeListFilterButton.swift
+++ b/Core/Core/Features/Grades/View/Header/GradeListFilterButton.swift
@@ -18,7 +18,29 @@
import SwiftUI
+@available(iOS, introduced: 26, message: "Legacy version exists")
struct GradeListFilterButton: View {
+ @Environment(\.viewController) var viewController
+ @ObservedObject var viewModel: GradeListViewModel
+
+ var body: some View {
+ if viewModel.state != .initialLoading {
+ Button {
+ viewModel.navigateToFilter(viewController: viewController)
+ } label: {
+ Image.filterLine
+ .size(24)
+ .offset(y: 2)
+ }
+ .accessibilityLabel(Text("Filter", bundle: .core))
+ .accessibilityHint(Text("Filter grades options", bundle: .core))
+ .accessibilityIdentifier("GradeList.filterButton")
+ }
+ }
+}
+
+@available(iOS, deprecated: 26, message: "Non-legacy version exists")
+struct LegacyGradeListFilterButton: View {
@Environment(\.viewController) var viewController
@ObservedObject var viewModel: GradeListViewModel
diff --git a/Core/Core/Features/Grades/View/Header/GradeListHeaderView.swift b/Core/Core/Features/Grades/View/Header/GradeListHeaderView.swift
index 47a890f3ce..d524302179 100644
--- a/Core/Core/Features/Grades/View/Header/GradeListHeaderView.swift
+++ b/Core/Core/Features/Grades/View/Header/GradeListHeaderView.swift
@@ -28,7 +28,7 @@ struct GradeListHeaderView: View {
HStack(alignment: .center) {
gradeDetailsView
if viewModel.isParentApp {
- GradeListFilterButton(viewModel: viewModel)
+ LegacyGradeListFilterButton(viewModel: viewModel)
.paddingStyle(.leading, .standard)
}
}
@@ -64,10 +64,15 @@ struct GradeListHeaderView: View {
}
.padding(.horizontal, 16)
.padding(.vertical, verticalSizeClass == .regular ? 20 : 5)
- .background(
- Color.backgroundLightest
- .cornerRadius(6)
- )
+ .background {
+ if #available(iOS 26, *) {
+ Color.backgroundLightest
+ .cornerRadius(24)
+ } else {
+ Color.backgroundLightest
+ .cornerRadius(6)
+ }
+ }
.shadow(color: Color.textDark.opacity(0.2), radius: 5, x: 0, y: 0)
}
diff --git a/Core/Core/Features/Inbox/ComposeMessage/View/ComposeMessageView.swift b/Core/Core/Features/Inbox/ComposeMessage/View/ComposeMessageView.swift
index 7351f2c16b..57520eb8e7 100644
--- a/Core/Core/Features/Inbox/ComposeMessage/View/ComposeMessageView.swift
+++ b/Core/Core/Features/Inbox/ComposeMessage/View/ComposeMessageView.swift
@@ -46,110 +46,198 @@ public struct ComposeMessageView: View, ScreenViewTrackable {
}
public var body: some View {
- InstUI.BaseScreen(state: model.state, config: model.screenConfig) { geometry in
- VStack(spacing: 0) {
- headerView
- .background(
- GeometryReader { proxy in
- Color.clear
- .onAppear {
- headerHeight = proxy.size.height
- model.showSearchRecipientsView = false
- focusedInput = nil
- }
- }
- )
- separator
- courseView
- separator
- ZStack(alignment: .topLeading) {
- VStack(spacing: 0) {
- propertiesView
- separator
-
- bodyView(geometry: geometry)
- attachmentsView
- if !model.includedMessages.isEmpty {
- includedMessages
- }
- // This Rectangle adds extra height to ensure smoother display of the list of recipients
- // without affecting the UI or any logic.
- Rectangle()
- .fill(Color.clear)
- .frame(height: 150)
- .allowsHitTesting(false)
- }
- if model.showSearchRecipientsView {
- RecipientFilterView(recipients: model.searchedRecipients) { selectedRecipient in
- model.showSearchRecipientsView = false
- model.textRecipientSearch = ""
- model.didSelectRecipient.accept(selectedRecipient)
- }
- .accessibilityHidden(true)
- .offset(y: model.recipients.isEmpty ? searchTextFieldHeight : recipientViewHeight + searchTextFieldHeight)
- .padding(.horizontal, 35)
- .fixedSize(horizontal: false, vertical: true)
- .animation(.smooth, value: model.showSearchRecipientsView)
- }
-
- }
- }
- .font(.regular12)
- .foregroundColor(.textDarkest)
- .background(
- GeometryReader { reader in
- Color
- .backgroundLightest
- .onTapGesture {
- model.clearSearchedRecipients()
- focusedInput = nil
- }
- .preference(key: ViewSizeKey.self, value: -reader.frame(in: .named("scroll")).origin.y)
- }
- )
- .navigationBarItems(leading: cancelButton, trailing: extraSendButton)
- .navigationBarStyle(.modal)
- }
- .onPreferenceChange(ViewSizeKey.self) { offset in
- model.showExtraSendButton = offset > headerHeight
- }
- .coordinateSpace(name: "scroll")
- .background(Color.backgroundLightest)
- .fileImporter(
- isPresented: $model.isFilePickerVisible,
- allowedContentTypes: [.item],
- allowsMultipleSelection: false
- ) { result in
- switch result {
- case .success(let urls):
- model.addFiles(urls: urls)
- case .failure:
- break
- }
- }
- .sheet(isPresented: $model.isImagePickerVisible) {
- AttachmentPickerAssembly.makeImagePicker(onSelect: model.addFile)
- }
- .sheet(isPresented: $model.isTakePhotoVisible) {
- AttachmentPickerAssembly.makeImageRecorder(onSelect: model.addFile)
- .interactiveDismissDisabled()
- }
- .sheet(isPresented: $model.isAudioRecordVisible) {
- AttachmentPickerAssembly.makeAudioRecorder(env: model.env, onSelect: model.addFile)
- .interactiveDismissDisabled()
- }
- .confirmationAlert(
- isPresented: $model.isShowingCancelDialog,
- presenting: model.confirmAlert
- )
- .confirmationAlert(
- isPresented: $model.isShowingErrorDialog,
- presenting: model.errorAlert
- )
+ if #available(iOS 26, *) {
+ InstUI.BaseScreen(state: model.state, config: model.screenConfig) { geometry in
+ baseScreenView(geometry)
+ .font(.regular12)
+ .foregroundColor(.textDarkest)
+ .background(
+ GeometryReader { reader in
+ Color
+ .backgroundLightest
+ .onTapGesture {
+ model.clearSearchedRecipients()
+ focusedInput = nil
+ }
+ .preference(key: ViewSizeKey.self, value: -reader.frame(in: .named("scroll")).origin.y)
+ }
+ )
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ cancelButton
+ }
+
+ ToolbarItem(placement: .topBarTrailing) {
+ extraSendButton
+ }
+ }
+ }
+ .onPreferenceChange(ViewSizeKey.self) { offset in
+ model.showExtraSendButton = offset > headerHeight
+ }
+ .coordinateSpace(name: "scroll")
+ .background(Color.backgroundLightest)
+ .fileImporter(
+ isPresented: $model.isFilePickerVisible,
+ allowedContentTypes: [.item],
+ allowsMultipleSelection: false
+ ) { result in
+ switch result {
+ case .success(let urls):
+ model.addFiles(urls: urls)
+ case .failure:
+ break
+ }
+ }
+ .sheet(isPresented: $model.isImagePickerVisible) {
+ AttachmentPickerAssembly.makeImagePicker(onSelect: model.addFile)
+ }
+ .sheet(isPresented: $model.isTakePhotoVisible) {
+ AttachmentPickerAssembly.makeImageRecorder(onSelect: model.addFile)
+ .interactiveDismissDisabled()
+ }
+ .sheet(isPresented: $model.isAudioRecordVisible) {
+ AttachmentPickerAssembly.makeAudioRecorder(env: model.env, onSelect: model.addFile)
+ .interactiveDismissDisabled()
+ }
+ .confirmationAlert(
+ isPresented: $model.isShowingCancelDialog,
+ presenting: model.confirmAlert
+ )
+ .confirmationAlert(
+ isPresented: $model.isShowingErrorDialog,
+ presenting: model.errorAlert
+ )
+ } else {
+ InstUI.BaseScreen(state: model.state, config: model.screenConfig) { geometry in
+ baseScreenView(geometry)
+ .font(.regular12)
+ .foregroundColor(.textDarkest)
+ .background(
+ GeometryReader { reader in
+ Color
+ .backgroundLightest
+ .onTapGesture {
+ model.clearSearchedRecipients()
+ focusedInput = nil
+ }
+ .preference(key: ViewSizeKey.self, value: -reader.frame(in: .named("scroll")).origin.y)
+ }
+ )
+ .navigationBarItems(leading: legacyCancelButton, trailing: legacyExtraSendButton)
+ .navigationBarStyle(.modal)
+ }
+ .onPreferenceChange(ViewSizeKey.self) { offset in
+ model.showExtraSendButton = offset > headerHeight
+ }
+ .coordinateSpace(name: "scroll")
+ .background(Color.backgroundLightest)
+ .fileImporter(
+ isPresented: $model.isFilePickerVisible,
+ allowedContentTypes: [.item],
+ allowsMultipleSelection: false
+ ) { result in
+ switch result {
+ case .success(let urls):
+ model.addFiles(urls: urls)
+ case .failure:
+ break
+ }
+ }
+ .sheet(isPresented: $model.isImagePickerVisible) {
+ AttachmentPickerAssembly.makeImagePicker(onSelect: model.addFile)
+ }
+ .sheet(isPresented: $model.isTakePhotoVisible) {
+ AttachmentPickerAssembly.makeImageRecorder(onSelect: model.addFile)
+ .interactiveDismissDisabled()
+ }
+ .sheet(isPresented: $model.isAudioRecordVisible) {
+ AttachmentPickerAssembly.makeAudioRecorder(env: model.env, onSelect: model.addFile)
+ .interactiveDismissDisabled()
+ }
+ .confirmationAlert(
+ isPresented: $model.isShowingCancelDialog,
+ presenting: model.confirmAlert
+ )
+ .confirmationAlert(
+ isPresented: $model.isShowingErrorDialog,
+ presenting: model.errorAlert
+ )
+ }
}
+ @ViewBuilder
+ private func baseScreenView(_ geometry: GeometryProxy) -> some View {
+ VStack(spacing: 0) {
+ headerView
+ .background(
+ GeometryReader { proxy in
+ Color.clear
+ .onAppear {
+ headerHeight = proxy.size.height
+ model.showSearchRecipientsView = false
+ focusedInput = nil
+ }
+ }
+ )
+ separator
+ courseView
+ separator
+ ZStack(alignment: .topLeading) {
+ VStack(spacing: 0) {
+ propertiesView
+ separator
+
+ bodyView(geometry: geometry)
+ attachmentsView
+ if !model.includedMessages.isEmpty {
+ includedMessages
+ }
+ // This Rectangle adds extra height to ensure smoother display of the list of recipients
+ // without affecting the UI or any logic.
+ Rectangle()
+ .fill(Color.clear)
+ .frame(height: 150)
+ .allowsHitTesting(false)
+ }
+ if model.showSearchRecipientsView {
+ RecipientFilterView(recipients: model.searchedRecipients) { selectedRecipient in
+ model.showSearchRecipientsView = false
+ model.textRecipientSearch = ""
+ model.didSelectRecipient.accept(selectedRecipient)
+ }
+ .accessibilityHidden(true)
+ .offset(y: model.recipients.isEmpty ? searchTextFieldHeight : recipientViewHeight + searchTextFieldHeight)
+ .padding(.horizontal, 35)
+ .fixedSize(horizontal: false, vertical: true)
+ .animation(.smooth, value: model.showSearchRecipientsView)
+ }
+
+ }
+ }
+ }
+
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ @ViewBuilder
+ private var extraSendButton: some View {
+ if model.showExtraSendButton {
+ Button {
+ model.didTapSend.accept(controller)
+ } label: {
+ Image.arrowUpLine
+ .resizable()
+ }
+ .buttonStyle(.glassProminent)
+ .accessibility(label: Text("Send", bundle: .core))
+ .disabled(!model.sendButtonActive)
+ .frame(maxHeight: .infinity, alignment: .top)
+ .accessibilityIdentifier("ComposeMessage.send")
+ }
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
@ViewBuilder
- private var extraSendButton: some View {
+ private var legacyExtraSendButton: some View {
if model.showExtraSendButton {
sendButton
} else {
@@ -162,7 +250,19 @@ public struct ComposeMessageView: View, ScreenViewTrackable {
.frame(height: 0.5)
}
- private var cancelButton: some View {
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ private var cancelButton: some View {
+ Button {
+ model.didTapCancel.accept(controller)
+ } label: {
+ Text("Cancel", bundle: .core)
+ .font(.regular16)
+ .accessibilityIdentifier("ComposeMessage.cancel")
+ }
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private var legacyCancelButton: some View {
Button {
model.didTapCancel.accept(controller)
} label: {
@@ -517,7 +617,9 @@ struct ComposeMessageView_Previews: PreviewProvider {
static let env = PreviewEnvironment()
static var previews: some View {
- ComposeMessageAssembly.makePreview(env: env)
+ NavigationStack {
+ ComposeMessageAssembly.makePreview(env: env)
+ }
}
}
diff --git a/Core/Core/Features/Inbox/MessageDetails/View/LegacyMessageView.swift b/Core/Core/Features/Inbox/MessageDetails/View/LegacyMessageView.swift
new file mode 100644
index 0000000000..df55ba5fef
--- /dev/null
+++ b/Core/Core/Features/Inbox/MessageDetails/View/LegacyMessageView.swift
@@ -0,0 +1,134 @@
+//
+// This file is part of Canvas.
+// Copyright (C) 2025-present Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+//
+
+import SwiftUI
+
+@available(iOS, deprecated: 26, message: "Non-legacy version exists")
+public struct LegacyMessageView: View {
+ @Environment(\.viewController) private var controller
+ @ScaledMetric private var uiScale: CGFloat = 1
+
+ private var model: MessageViewModel
+ private let isReplyButtonVisible: Bool
+ private var replyDidTap: () -> Void
+ private var moreDidTap: () -> Void
+
+ public init(
+ model: MessageViewModel,
+ isReplyButtonVisible: Bool,
+ replyDidTap: @escaping () -> Void,
+ moreDidTap: @escaping () -> Void
+ ) {
+ self.model = model
+ self.replyDidTap = replyDidTap
+ self.moreDidTap = moreDidTap
+ self.isReplyButtonVisible = isReplyButtonVisible
+ }
+
+ public var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ headerView
+ bodyView
+ if isReplyButtonVisible {
+ replyButton
+ }
+ }
+ }
+
+ private var replyButton: some View {
+ Button {
+ replyDidTap()
+ } label: {
+ Text("Reply", bundle: .core)
+ .font(.regular16)
+ .foregroundColor(Color(Brand.shared.linkColor))
+ .accessibilityIdentifier("MessageDetails.replyButton")
+ }
+ }
+
+ private var headerView: some View {
+ HStack(alignment: .top, spacing: 4) {
+ Avatar(name: model.avatarName, url: model.avatarURL, size: 36, isAccessible: false)
+ VStack(alignment: .leading) {
+ Text(model.author)
+ .font(.regular16)
+ .foregroundColor(.textDarkest)
+ .lineLimit(1)
+ .accessibilityIdentifier("MessageDetails.author")
+
+ Text(model.date)
+ .foregroundColor(.textDark)
+ .font(.regular12)
+ .accessibilityIdentifier("MessageDetails.date")
+ }
+ Spacer()
+ if isReplyButtonVisible {
+ replyIconButton
+ }
+ Button {
+ moreDidTap()
+ } label: {
+ Image
+ .moreLine
+ .size(uiScale.iconScale * 20)
+ .foregroundColor(.textDark)
+ .padding(.horizontal, 6)
+ .accessibilityLabel(Text("Conversation options", bundle: .core))
+ .accessibilityIdentifier("MessageDetails.options")
+ }
+ }
+ }
+
+ private var replyIconButton: some View {
+ Button {
+ replyDidTap()
+ } label: {
+ Image
+ .replyLine
+ .size(uiScale.iconScale * 20)
+ .foregroundColor(.textDark)
+ .padding(.leading, 6)
+ .accessibilityLabel(Text("Reply", bundle: .core))
+ .accessibilityIdentifier("MessageDetails.replyImage")
+ }
+ }
+
+ private var bodyView: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ SelectableText(
+ attributedText: model.body.toAttributedStringWithLinks(),
+ font: .regular16,
+ lineHeight: .fit,
+ textColor: .textDarkest
+ )
+ .accessibilityIdentifier("MessageDetails.body")
+
+ if model.showAttachments {
+ AttachmentsView(
+ attachments: model.attachments,
+ mediaComment: model.mediaComment,
+ didSelectAttachment: model.handleFileNavigation
+ )
+ }
+ }
+ .onAppear {
+ model.controller = controller
+ }
+ .environment(\.openURL, OpenURLAction(handler: model.handleURL))
+ }
+}
diff --git a/Core/Core/Features/Inbox/MessageDetails/View/MessageDetailsView.swift b/Core/Core/Features/Inbox/MessageDetails/View/MessageDetailsView.swift
index 50461cb043..0606b5feee 100644
--- a/Core/Core/Features/Inbox/MessageDetails/View/MessageDetailsView.swift
+++ b/Core/Core/Features/Inbox/MessageDetails/View/MessageDetailsView.swift
@@ -28,32 +28,60 @@ public struct MessageDetailsView: View {
}
public var body: some View {
- RefreshableScrollView {
- switch model.state {
- case .loading:
- loadingIndicator
- case .data:
- detailsView
- case .empty, .error:
- VStack(alignment: .center, spacing: 0) {
- Text("There was an error loading the message. Pull to refresh to try again.", bundle: .core)
- .multilineTextAlignment(.center)
- .padding(.horizontal, 12)
- }
- .padding(.horizontal, 12)
- .padding(.vertical, 24)
- }
- }
- refreshAction: { onComplete in
- model.refreshDidTrigger.send {
- onComplete()
- }
- }
- .background(Color.backgroundLightest)
- .navigationBarTitleView(model.title)
- .navigationBarItems(trailing: moreButton)
- .navigationBarStyle(.global)
- .snackBar(viewModel: model.snackBarViewModel)
+ if #available(iOS 26, *) {
+ RefreshableScrollView {
+ switch model.state {
+ case .loading:
+ loadingIndicator
+ case .data:
+ detailsView
+ case .empty, .error:
+ VStack(alignment: .center, spacing: 0) {
+ Text("There was an error loading the message. Pull to refresh to try again.", bundle: .core)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal, 12)
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 24)
+ }
+ }
+ refreshAction: { onComplete in
+ model.refreshDidTrigger.send {
+ onComplete()
+ }
+ }
+ .background(Color.backgroundLightest)
+ .navigationTitle(model.title)
+ .snackBar(viewModel: model.snackBarViewModel)
+ .toolbar { moreButton }
+ } else {
+ RefreshableScrollView {
+ switch model.state {
+ case .loading:
+ loadingIndicator
+ case .data:
+ detailsView
+ case .empty, .error:
+ VStack(alignment: .center, spacing: 0) {
+ Text("There was an error loading the message. Pull to refresh to try again.", bundle: .core)
+ .multilineTextAlignment(.center)
+ .padding(.horizontal, 12)
+ }
+ .padding(.horizontal, 12)
+ .padding(.vertical, 24)
+ }
+ }
+ refreshAction: { onComplete in
+ model.refreshDidTrigger.send {
+ onComplete()
+ }
+ }
+ .background(Color.backgroundLightest)
+ .navigationBarTitleView(model.title)
+ .navigationBarItems(trailing: legacyMoreButton)
+ .navigationBarStyle(.global)
+ .snackBar(viewModel: model.snackBarViewModel)
+ }
}
private var loadingIndicator: some View {
@@ -89,7 +117,71 @@ public struct MessageDetailsView: View {
.padding(.horizontal, 16)
}
- private var moreButton: some View {
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ private var moreButton: some View {
+ Menu {
+ if model.isReplyButtonVisible {
+ Button(.init("Reply", bundle: .core), image: .replyLine) {
+ model.replyTapped(message: nil, viewController: controller)
+ }
+ .accessibilityIdentifier("MessageDetails.reply")
+ }
+
+ if !model.isStudentAccessRestricted {
+ Button(.init("Reply All", bundle: .core), image: .replyAllLine) {
+ model.replyAllTapped(message: nil, viewController: controller)
+ }
+ .accessibilityIdentifier("MessageDetails.replyAll")
+ }
+
+ Button(.init("Forward", bundle: .core), image: .forwardLine) {
+ model.forwardTapped(viewController: controller)
+ }
+ .accessibilityIdentifier("MessageDetails.forward")
+
+ if model.conversations.first?.workflowState == .read {
+ Button(.init("Mark as Unread", bundle: .core), image: .nextUnreadLine) {
+ model.updateState.send(.unread)
+ }
+ .accessibilityIdentifier("MessageDetails.markAsUnread")
+ } else {
+ Button(.init("Mark as Read", bundle: .core), image: .emailLine) {
+ model.updateState.send(.read)
+ }
+ .accessibilityIdentifier("MessageDetails.markAsRead")
+ }
+
+ if model.conversations.first?.workflowState != .archived, model.allowArchive {
+ Button(.init("Archive", bundle: .core), image: .archiveLine) {
+ model.updateState.send(.archived)
+ }
+ .accessibilityIdentifier("MessageDetails.archive")
+ }
+
+ if model.conversations.first?.workflowState == .archived, model.allowArchive {
+ Button(.init("Unarchive", bundle: .core), image: .unarchiveLine) {
+ model.updateState.send(.read)
+ }
+ .accessibilityIdentifier("MessageDetails.unarchive")
+ }
+
+ if !model.isStudentAccessRestricted {
+ Button(.init("Delete Conversation", bundle: .core), image: .trashLine) {
+ if let conversationId = model.conversations.first?.id {
+ model.deleteConversationDidTap.send((conversationId, controller))
+ }
+ }
+ .accessibilityIdentifier("MessageDetails.delete")
+ }
+ } label: {
+ Image.moreSolid
+ }
+ .accessibilityIdentifier("MessageDetails.more")
+ .accessibility(label: Text("More options", bundle: .core))
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private var legacyMoreButton: some View {
Button(action: {
model.conversationMoreTapped(viewController: controller)
}, label: {
@@ -133,12 +225,24 @@ public struct MessageDetailsView: View {
Color.borderMedium
.frame(height: 0.5)
- MessageView(model: message,
- isReplyButtonVisible: model.isReplyButtonVisible,
- replyDidTap: { model.replyTapped(message: message.conversationMessage, viewController: controller) },
- moreDidTap: { model.messageMoreTapped(message: message.conversationMessage, viewController: controller) })
- .padding(16)
+ if #available(iOS 26, *) {
+ MessageView(model: message,
+ isReplyButtonVisible: model.isReplyButtonVisible,
+ isStudentAccessRestricted: model.isStudentAccessRestricted,
+ replyDidTap: { model.messageReplyTapped(message: message.conversationMessage, viewController: controller) },
+ replyAllDidTap: { model.messageReplyAllTapped(message: message.conversationMessage, viewController: controller) },
+ forwardDidTap: { model.forwardTapped(message: message.conversationMessage, viewController: controller)},
+ deleteDidTap: { model.deleteMessageTapped(message: message.conversationMessage, viewController: controller) }
+ )
+ .padding(16)
+ } else {
+ LegacyMessageView(model: message,
+ isReplyButtonVisible: model.isReplyButtonVisible,
+ replyDidTap: { model.replyTapped(message: message.conversationMessage, viewController: controller) },
+ moreDidTap: { model.messageMoreTapped(message: message.conversationMessage, viewController: controller) })
+ .padding(16)
+ }
}
}
}
@@ -151,7 +255,9 @@ struct MessageDetailsView_Previews: PreviewProvider {
static let context = env.globalDatabase.viewContext
static var previews: some View {
- MessageDetailsAssembly.makePreview(env: env, subject: "Message Title", messages: .make(count: 5, body: InstUI.PreviewData.loremIpsumLong, in: context))
+ NavigationStack {
+ MessageDetailsAssembly.makePreview(env: env, subject: "Message Title", messages: .make(count: 5, body: InstUI.PreviewData.loremIpsumLong, in: context))
+ }
}
}
diff --git a/Core/Core/Features/Inbox/MessageDetails/View/MessageView.swift b/Core/Core/Features/Inbox/MessageDetails/View/MessageView.swift
index 851ff292c2..9e61dabf64 100644
--- a/Core/Core/Features/Inbox/MessageDetails/View/MessageView.swift
+++ b/Core/Core/Features/Inbox/MessageDetails/View/MessageView.swift
@@ -18,25 +18,35 @@
import SwiftUI
+@available(iOS, introduced: 26, message: "Legacy version exists")
public struct MessageView: View {
@Environment(\.viewController) private var controller
@ScaledMetric private var uiScale: CGFloat = 1
private var model: MessageViewModel
private let isReplyButtonVisible: Bool
- private var replyDidTap: () -> Void
- private var moreDidTap: () -> Void
+ private let isStudentAccessRestricted: Bool
+ private let replyDidTap: () -> Void
+ private let replyAllDidTap: () -> Void
+ private let forwardDidTap: () -> Void
+ public var deleteDidTap: () -> Void
public init(
model: MessageViewModel,
isReplyButtonVisible: Bool,
+ isStudentAccessRestricted: Bool,
replyDidTap: @escaping () -> Void,
- moreDidTap: @escaping () -> Void
+ replyAllDidTap: @escaping () -> Void,
+ forwardDidTap: @escaping () -> Void,
+ deleteDidTap: @escaping () -> Void
) {
self.model = model
self.replyDidTap = replyDidTap
- self.moreDidTap = moreDidTap
+ self.isStudentAccessRestricted = isStudentAccessRestricted
+ self.replyAllDidTap = replyAllDidTap
self.isReplyButtonVisible = isReplyButtonVisible
+ self.forwardDidTap = forwardDidTap
+ self.deleteDidTap = deleteDidTap
}
public var body: some View {
@@ -79,8 +89,32 @@ public struct MessageView: View {
if isReplyButtonVisible {
replyIconButton
}
- Button {
- moreDidTap()
+ Menu {
+ if isReplyButtonVisible {
+ Button(.init("Reply", bundle: .core), image: .replyLine, action: replyDidTap)
+ .accessibilityIdentifier("MessageDetails.reply")
+
+ if !isStudentAccessRestricted {
+ Button(
+ .init("Reply All", bundle: .core),
+ image: .replyAllLine,
+ action: replyAllDidTap
+ )
+ .accessibilityIdentifier("MessageDetails.replyAll")
+ }
+
+ Button(.init("Forward", bundle: .core), image: .forwardLine, action: forwardDidTap)
+ .accessibilityIdentifier("MessageDetails.forward")
+
+ if !isStudentAccessRestricted {
+ Button(
+ .init("Delete Message", bundle: .core),
+ image: .trashLine,
+ action: deleteDidTap
+ )
+ .accessibilityIdentifier("MessageDetails.delete")
+ }
+ }
} label: {
Image
.moreLine
@@ -131,3 +165,18 @@ public struct MessageView: View {
.environment(\.openURL, OpenURLAction(handler: model.handleURL))
}
}
+
+#if DEBUG
+
+#Preview {
+ let env = PreviewEnvironment()
+ let context = env.globalDatabase.viewContext
+
+ MessageDetailsAssembly.makePreview(
+ env: env,
+ subject: "Message Title",
+ messages: .make(count: 5, body: InstUI.PreviewData.loremIpsumLong, in: context)
+ )
+}
+
+#endif
diff --git a/Core/Core/Features/Inbox/MessageDetails/ViewModel/MessageDetailsViewModel.swift b/Core/Core/Features/Inbox/MessageDetails/ViewModel/MessageDetailsViewModel.swift
index bebc740cf1..e34e8c14c1 100644
--- a/Core/Core/Features/Inbox/MessageDetails/ViewModel/MessageDetailsViewModel.swift
+++ b/Core/Core/Features/Inbox/MessageDetails/ViewModel/MessageDetailsViewModel.swift
@@ -28,6 +28,7 @@ class MessageDetailsViewModel: ObservableObject {
@Published public private(set) var starred: Bool = false
@Published public private(set) var isReplyButtonVisible: Bool = false
@Published public private(set) var isStudentAccessRestricted: Bool = false
+ public let allowArchive: Bool
public let snackBarViewModel = SnackBarViewModel()
@@ -55,7 +56,6 @@ class MessageDetailsViewModel: ObservableObject {
private let studentAccessInteractor: StudentAccessInteractor
private let env: AppEnvironment
private let myID: String
- private let allowArchive: Bool
public init(interactor: MessageDetailsInteractor, studentAccessInteractor: StudentAccessInteractor, myID: String, allowArchive: Bool, env: AppEnvironment) {
self.interactor = interactor
@@ -69,6 +69,7 @@ class MessageDetailsViewModel: ObservableObject {
bindStudentAccessRestriction()
}
+ @available(iOS, deprecated: 26)
public func conversationMoreTapped(viewController: WeakViewController) {
let sheet = BottomSheetPickerViewController.create()
if isReplyButtonVisible {
@@ -143,6 +144,28 @@ class MessageDetailsViewModel: ObservableObject {
env.router.show(sheet, from: viewController, options: .modal())
}
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ public func messageReplyTapped(message: ConversationMessage?, viewController: WeakViewController) {
+ if let message {
+ replyTapped(message: message, viewController: viewController)
+ }
+ }
+
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ public func messageReplyAllTapped(message: ConversationMessage?, viewController: WeakViewController) {
+ if let message {
+ replyAllTapped(message: message, viewController: viewController)
+ }
+ }
+
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ public func deleteMessageTapped(message: ConversationMessage?, viewController: WeakViewController) {
+ if let conversationId = conversations.first?.id, let messageId = message?.id {
+ deleteConversationMessageDidTap.send((conversationId: conversationId, messageId: messageId, viewController: viewController))
+ }
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
public func messageMoreTapped(message: ConversationMessage?, viewController: WeakViewController) {
let sheet = BottomSheetPickerViewController.create()
if isReplyButtonVisible {
diff --git a/Core/Core/Features/Inbox/View/InboxView.swift b/Core/Core/Features/Inbox/View/InboxView.swift
index e453521649..7675b829c7 100644
--- a/Core/Core/Features/Inbox/View/InboxView.swift
+++ b/Core/Core/Features/Inbox/View/InboxView.swift
@@ -35,46 +35,98 @@ public struct InboxView: View, ScreenViewTrackable {
}
public var body: some View {
- VStack(spacing: 0) {
- InboxFilterBarView(model: model)
- Color.borderMedium
- .frame(height: 0.5)
- if case .loading = model.state {
- loadingIndicator
- } else {
- GeometryReader { geometry in
- List {
- switch model.state {
- case .data:
- messagesList
- .listRowBackground(SwiftUI.EmptyView())
- nextPageLoadingIndicator(geometry: geometry)
- .onAppear {
- model.contentDidScrollToBottom.send()
- }
- case .empty:
- panda(geometry: geometry, data: model.emptyState)
- case .error:
- panda(geometry: geometry, data: model.errorState)
- case .loading:
- SwiftUI.EmptyView()
- }
- }
- .refreshable {
- await withCheckedContinuation { continuation in
- model.refreshDidTrigger.send {
- continuation.resume()
- }
- }
- }
- .listStyle(PlainListStyle())
- .animation(.default, value: model.messages)
- }
- }
- }
- .background(Color.backgroundLightest)
- .navigationBarItems(leading: model.isShowMenuButton ? menuButton : nil, trailing: newMessageButton)
- .navigationBarStyle(.global)
+ if #available(iOS 26, *) {
+ VStack(spacing: 0) {
+ InboxFilterBarView(model: model)
+ Color.borderMedium
+ .frame(height: 0.5)
+ if case .loading = model.state {
+ loadingIndicator
+ } else {
+ GeometryReader { geometry in
+ List {
+ switch model.state {
+ case .data:
+ messagesList
+ .listRowBackground(SwiftUI.EmptyView())
+ nextPageLoadingIndicator(geometry: geometry)
+ .onAppear {
+ model.contentDidScrollToBottom.send()
+ }
+ case .empty:
+ panda(geometry: geometry, data: model.emptyState)
+ case .error:
+ panda(geometry: geometry, data: model.errorState)
+ case .loading:
+ SwiftUI.EmptyView()
+ }
+ }
+ .refreshable {
+ await withCheckedContinuation { continuation in
+ model.refreshDidTrigger.send {
+ continuation.resume()
+ }
+ }
+ }
+ .listStyle(PlainListStyle())
+ .animation(.default, value: model.messages)
+ }
+ }
+ }
+ .background(Color.backgroundLightest)
+ .toolbar {
+ if model.isShowMenuButton {
+ ToolbarItem(placement: .topBarLeading) {
+ menuButton
+ }
+ }
+
+ ToolbarItem(placement: .topBarTrailing) {
+ newMessageButton
+ }
+ }
+ } else {
+ VStack(spacing: 0) {
+ InboxFilterBarView(model: model)
+ Color.borderMedium
+ .frame(height: 0.5)
+ if case .loading = model.state {
+ loadingIndicator
+ } else {
+ GeometryReader { geometry in
+ List {
+ switch model.state {
+ case .data:
+ messagesList
+ .listRowBackground(SwiftUI.EmptyView())
+ nextPageLoadingIndicator(geometry: geometry)
+ .onAppear {
+ model.contentDidScrollToBottom.send()
+ }
+ case .empty:
+ panda(geometry: geometry, data: model.emptyState)
+ case .error:
+ panda(geometry: geometry, data: model.errorState)
+ case .loading:
+ SwiftUI.EmptyView()
+ }
+ }
+ .refreshable {
+ await withCheckedContinuation { continuation in
+ model.refreshDidTrigger.send {
+ continuation.resume()
+ }
+ }
+ }
+ .listStyle(PlainListStyle())
+ .animation(.default, value: model.messages)
+ }
+ }
+ }
+ .background(Color.backgroundLightest)
+ .navigationBarItems(leading: model.isShowMenuButton ? legacyMenuButton : nil, trailing: legacyNewMessageButton)
+ .navigationBarStyle(.global)
+ }
}
private var messagesList: some View {
@@ -209,7 +261,20 @@ public struct InboxView: View, ScreenViewTrackable {
.listRowSeparator(.hidden)
}
- private var menuButton: some View {
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ private var menuButton: some View {
+ Button {
+ model.menuDidTap.send(controller)
+ } label: {
+ // TODO: Remove the condition once horizon-specific logic is no longer needed.
+ Image.hamburgerSolid
+ }
+ .identifier("Inbox.profileButton")
+ .accessibility(label: Text("Profile Menu, Closed", bundle: .core, comment: "Accessibility text describing the Profile Menu button and its state"))
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private var legacyMenuButton: some View {
Button {
model.menuDidTap.send(controller)
} label: {
@@ -222,7 +287,20 @@ public struct InboxView: View, ScreenViewTrackable {
.accessibility(label: Text("Profile Menu, Closed", bundle: .core, comment: "Accessibility text describing the Profile Menu button and its state"))
}
- private var newMessageButton: some View {
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ private var newMessageButton: some View {
+ Button {
+ model.newMessageDidTap.send(controller)
+ } label: {
+ Image.addSolid
+ // TODO: Remove the condition once horizon-specific logic is no longer needed.
+ }
+ .identifier("Inbox.newMessageButton")
+ .accessibility(label: Text("New Message", bundle: .core))
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ private var legacyNewMessageButton: some View {
Button {
model.newMessageDidTap.send(controller)
} label: {
diff --git a/Core/Core/Features/LTI/View/LTIViewController.swift b/Core/Core/Features/LTI/View/LTIViewController.swift
index c04c43b509..6494e31592 100644
--- a/Core/Core/Features/LTI/View/LTIViewController.swift
+++ b/Core/Core/Features/LTI/View/LTIViewController.swift
@@ -60,7 +60,13 @@ public class LTIViewController: UIViewController, ErrorViewController, ColoredNa
scrollView.backgroundColor = .backgroundLightest
spinnerView.isHidden = true
nameLabel.text = name ?? String(localized: "LTI Tool", bundle: .core)
- setupTitleViewInNavbar(title: String(localized: "External Tool", bundle: .core))
+
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "External Tool", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "External Tool", bundle: .core))
+ }
+
if name == nil {
// try to get a more descriptive name of the tool
tools.getSessionlessLaunch { [weak self] response in
@@ -86,7 +92,12 @@ public class LTIViewController: UIViewController, ErrorViewController, ColoredNa
return
}
let course = courses?.first
- updateNavBar(subtitle: course?.name, color: course?.color)
+
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = course?.name
+ } else {
+ updateNavBar(subtitle: course?.name, color: course?.color)
+ }
}
@IBAction func openButtonPressed(_ sender: UIButton) {
diff --git a/Core/Core/Features/Login/LoginFindSchoolViewController.swift b/Core/Core/Features/Login/LoginFindSchoolViewController.swift
index c028e3e4a9..167a556144 100644
--- a/Core/Core/Features/Login/LoginFindSchoolViewController.swift
+++ b/Core/Core/Features/Login/LoginFindSchoolViewController.swift
@@ -25,7 +25,11 @@ class LoginFindSchoolViewController: UIViewController {
@IBOutlet weak var resultsTableView: UITableView!
@IBOutlet weak var searchField: UITextField!
- private lazy var nextButton = UIBarButtonItem(title: String(localized: "Next", bundle: .core), style: .done, target: self, action: #selector(nextPressed))
+ private lazy var nextButton = if #available(iOS 26, *) {
+ UIBarButtonItem(title: String(localized: "Next", bundle: .core), style: .prominent, target: self, action: #selector(nextPressed))
+ } else {
+ UIBarButtonItem(title: String(localized: "Next", bundle: .core), style: .done, target: self, action: #selector(nextPressed))
+ }
var accounts = [APIAccountResult]()
var api: API = API()
diff --git a/Core/Core/Features/Login/LoginNavigationController.swift b/Core/Core/Features/Login/LoginNavigationController.swift
index 677ffbcb9e..b9442c6095 100644
--- a/Core/Core/Features/Login/LoginNavigationController.swift
+++ b/Core/Core/Features/Login/LoginNavigationController.swift
@@ -32,7 +32,9 @@ public class LoginNavigationController: UINavigationController {
public override func viewDidLoad() {
super.viewDidLoad()
- navigationBar.useStyle(.global)
+ if #unavailable(iOS 26) {
+ navigationBar.useStyle(.global)
+ }
isNavigationBarHidden = true
}
diff --git a/Core/Core/Features/Modules/ModuleItems/ModuleItemDetailsViewController.swift b/Core/Core/Features/Modules/ModuleItems/ModuleItemDetailsViewController.swift
index 675fcb6ac5..65a8e4dd98 100644
--- a/Core/Core/Features/Modules/ModuleItems/ModuleItemDetailsViewController.swift
+++ b/Core/Core/Features/Modules/ModuleItems/ModuleItemDetailsViewController.swift
@@ -64,7 +64,11 @@ public final class ModuleItemDetailsViewController: UIViewController, ColoredNav
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .backgroundLightest
- setupTitleViewInNavbar(title: String(localized: "Module Item", bundle: .core))
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Module Item", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Module Item", bundle: .core))
+ }
Analytics.shared.logEvent("module_item", parameters: ["moduleID": moduleID!, "itemID": itemID!])
errorView.isHidden = true
errorView.retryButton.addTarget(self, action: #selector(retryButtonPressed), for: .primaryActionTriggered)
@@ -107,7 +111,12 @@ public final class ModuleItemDetailsViewController: UIViewController, ColoredNav
// When embedded view controllers adapt course color for their own spinner view,
// we should enable this line below.
// spinnerView.color = course.first?.color
- updateNavBar(subtitle: course.first?.name, color: course.first?.color)
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = course.first?.name
+ } else {
+ updateNavBar(subtitle: course.first?.name, color: course.first?.color)
+ }
+
let title: String
switch item?.type {
case .assignment:
@@ -127,7 +136,13 @@ public final class ModuleItemDetailsViewController: UIViewController, ColoredNav
case nil, .subHeader:
title = String(localized: "Module Item", bundle: .core)
}
- setupTitleViewInNavbar(title: title)
+
+ if #available(iOS 26, *) {
+ navigationItem.title = title
+ } else {
+ setupTitleViewInNavbar(title: title)
+ }
+
if item?.completionRequirementType == .must_mark_done {
navigationItem.rightBarButtonItems = []
navigationItem.rightBarButtonItems?.append(optionsButton)
diff --git a/Core/Core/Features/Modules/ModuleItems/ModuleItemSequenceViewController.swift b/Core/Core/Features/Modules/ModuleItems/ModuleItemSequenceViewController.swift
index 01b278d3f7..b878856d85 100644
--- a/Core/Core/Features/Modules/ModuleItems/ModuleItemSequenceViewController.swift
+++ b/Core/Core/Features/Modules/ModuleItems/ModuleItemSequenceViewController.swift
@@ -69,6 +69,15 @@ public final class ModuleItemSequenceViewController: UIViewController {
leftBarButtonItems = navigationItem.leftBarButtonItems
rightBarButtonItems = navigationItem.rightBarButtonItems
+ if #available(iOS 26, *) {
+ // Since titleSubtitleView is in use, we need to set the toolbar appearance to match the screen's
+ let appearance = UINavigationBarAppearance()
+ appearance.configureWithDefaultBackground()
+ appearance.backgroundColor = .backgroundLightest
+ navigationController?.navigationBar.standardAppearance = appearance
+ navigationController?.navigationBar.scrollEdgeAppearance = appearance
+ }
+
showSequenceButtons(prev: false, next: false)
pages.scrollView.isScrollEnabled = false
embed(pages, in: pagesContainer)
@@ -97,6 +106,18 @@ public final class ModuleItemSequenceViewController: UIViewController {
store.refresh(force: true)
}
+ public override func viewWillDisappear(_ animated: Bool) {
+ if #available(iOS 26, *) {
+ // Remove the toolbar appearance override since it is for this screen only
+ let appearance = UINavigationBarAppearance()
+ appearance.configureWithDefaultBackground()
+ navigationController?.navigationBar.standardAppearance = appearance
+ navigationController?.navigationBar.scrollEdgeAppearance = appearance
+ }
+
+ super.viewWillDisappear(animated)
+ }
+
private func update(embed: Bool) {
if store.requested, store.pending {
return
diff --git a/Core/Core/Features/Modules/ModuleList/ModuleListViewController.swift b/Core/Core/Features/Modules/ModuleList/ModuleListViewController.swift
index afff2c0507..648581dc78 100644
--- a/Core/Core/Features/Modules/ModuleList/ModuleListViewController.swift
+++ b/Core/Core/Features/Modules/ModuleList/ModuleListViewController.swift
@@ -71,7 +71,12 @@ public final class ModuleListViewController: ScreenViewTrackableViewController,
public override func viewDidLoad() {
super.viewDidLoad()
- setupTitleViewInNavbar(title: String(localized: "Modules", bundle: .core))
+
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Modules", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Modules", bundle: .core))
+ }
collapsedIDs[courseID] = collapsedIDs[courseID] ?? []
if let moduleID = moduleID {
@@ -111,7 +116,9 @@ public final class ModuleListViewController: ScreenViewTrackableViewController,
if let selectedIndexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: selectedIndexPath, animated: false)
}
- navigationController?.navigationBar.useContextColor(color)
+ if #unavailable(iOS 26) {
+ navigationController?.navigationBar.useContextColor(color)
+ }
}
private func update() {
@@ -176,7 +183,11 @@ public final class ModuleListViewController: ScreenViewTrackableViewController,
}
private func reloadCourse() {
- updateNavBar(subtitle: courses.first?.name, color: courses.first?.color)
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = courses.first?.name
+ } else {
+ updateNavBar(subtitle: courses.first?.name, color: courses.first?.color)
+ }
view.tintColor = color
}
diff --git a/Core/Core/Features/Pages/PageDetails/PageDetailsViewController.swift b/Core/Core/Features/Pages/PageDetails/PageDetailsViewController.swift
index 20d39be5f1..0ecf3ab29c 100644
--- a/Core/Core/Features/Pages/PageDetails/PageDetailsViewController.swift
+++ b/Core/Core/Features/Pages/PageDetails/PageDetailsViewController.swift
@@ -80,7 +80,13 @@ open class PageDetailsViewController: UIViewController, ColoredNavViewProtocol,
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .backgroundLightest
- setupTitleViewInNavbar(title: String(localized: "Page Details", bundle: .core))
+
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Page Details", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Page Details", bundle: .core))
+ }
+
webViewContainer.addSubview(webView)
webView.pinWithThemeSwitchButton(inside: webViewContainer)
webView.linkDelegate = self
@@ -132,12 +138,21 @@ open class PageDetailsViewController: UIViewController, ColoredNavViewProtocol,
let color = context.contextType == .course ? courses.first?.color : groups.first?.color,
env.app != .parent
else { return }
- updateNavBar(subtitle: name, color: color)
+
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = name
+ } else {
+ updateNavBar(subtitle: name, color: color)
+ }
}
private func update() {
guard let page = page else { return }
- setupTitleViewInNavbar(title: page.title)
+ if #available(iOS 26, *) {
+ navigationItem.title = page.title
+ } else {
+ setupTitleViewInNavbar(title: page.title)
+ }
optionsButton.accessibilityIdentifier = "PageDetails.options"
navigationItem.rightBarButtonItem = canEdit ? optionsButton : nil
diff --git a/Core/Core/Features/Pages/PageList/PageListViewController.swift b/Core/Core/Features/Pages/PageList/PageListViewController.swift
index 593aa3a572..21d038588f 100644
--- a/Core/Core/Features/Pages/PageList/PageListViewController.swift
+++ b/Core/Core/Features/Pages/PageList/PageListViewController.swift
@@ -64,8 +64,14 @@ public class PageListViewController: ScreenViewTrackableViewController, ColoredN
public override func viewDidLoad() {
super.viewDidLoad()
- setupTitleViewInNavbar(title: String(localized: "Pages", bundle: .core))
- if canCreatePage {
+
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Pages", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Pages", bundle: .core))
+ }
+
+ if canCreatePage {
let item = UIBarButtonItem(image: .addSolid, style: .plain, target: self, action: #selector(createPage))
item.accessibilityIdentifier = "PageList.add"
navigationItem.rightBarButtonItem = item
@@ -111,7 +117,11 @@ public class PageListViewController: ScreenViewTrackableViewController, ColoredN
loadingView.color = color
refreshControl.color = color
view.tintColor = color
- updateNavBar(subtitle: name, color: color)
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = name
+ } else {
+ updateNavBar(subtitle: name, color: color)
+ }
}
func update() {
@@ -210,6 +220,12 @@ extension PageListViewController: UITableViewDataSource, UITableViewDelegate {
if cell is LoadingCell {
pages.getNextPage()
}
+
+ if #available(iOS 26, *) {
+ if indexPath.section == 0 && indexPath.row == 0 {
+ cell.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: .greatestFiniteMagnitude)
+ }
+ }
}
public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
diff --git a/Core/Core/Features/People/ContextCard/Course/ContextCardView.swift b/Core/Core/Features/People/ContextCard/Course/ContextCardView.swift
index 915643dd7a..60ef834041 100644
--- a/Core/Core/Features/People/ContextCard/Course/ContextCardView.swift
+++ b/Core/Core/Features/People/ContextCard/Course/ContextCardView.swift
@@ -27,20 +27,48 @@ public struct ContextCardView: View {
}
public var body: some View {
- contextCard
- .background(Color.backgroundLightest)
- .navigationBarTitleView(
- title: model.user.first?.name ?? "",
- subtitle: model.course.first?.name
- )
- .navigationBarItems(trailing: emailButton)
- .navigationBarStyle(model.isModal ? .modal : .color(nil))
- .onAppear {
- model.viewAppeared()
- }
+ if #available(iOS 26, *) {
+ contextCard
+ .background(Color.backgroundLightest)
+ .optionalNavigationTitle(model.user.first?.name)
+ .optionalNavigationSubtitle(model.course.first?.name)
+ .toolbar {
+ emailButton
+ }
+ .navigationBarStyle(model.isModal ? .modal : .color(nil))
+ .onAppear {
+ model.viewAppeared()
+ }
+ } else {
+ contextCard
+ .background(Color.backgroundLightest)
+ .navigationBarTitleView(
+ title: model.user.first?.name ?? "",
+ subtitle: model.course.first?.name
+ )
+ .navigationBarItems(trailing: legacyEmailButton)
+ .navigationBarStyle(model.isModal ? .modal : .color(nil))
+ .onAppear {
+ model.viewAppeared()
+ }
+ }
}
- @ViewBuilder var emailButton: some View {
+ @available(iOS, introduced: 26, message: "Legacy version exists")
+ @ViewBuilder
+ var emailButton: some View {
+ if model.permissions.first?.sendMessages == true, model.isViewingAnotherUser {
+ Button { model.openNewMessageComposer(controller: controller.value) } label: {
+ Image.emailLine
+ }
+ .accessibility(label: Text("Send message", bundle: .core))
+ .identifier("ContextCard.emailContact")
+ }
+ }
+
+ @available(iOS, deprecated: 26, message: "Non-legacy version exists")
+ @ViewBuilder
+ var legacyEmailButton: some View {
if model.permissions.first?.sendMessages == true, model.isViewingAnotherUser {
Button(action: { model.openNewMessageComposer(controller: controller.value) }, label: {
let color = model.isModal ? Brand.shared.buttonPrimaryBackground : Brand.shared.buttonPrimaryText
diff --git a/Core/Core/Features/People/PeopleListViewController.swift b/Core/Core/Features/People/PeopleListViewController.swift
index fa423e8ed2..8edc76e267 100644
--- a/Core/Core/Features/People/PeopleListViewController.swift
+++ b/Core/Core/Features/People/PeopleListViewController.swift
@@ -46,12 +46,16 @@ public class PeopleListViewController: ScreenViewTrackableViewController, Colore
lazy var customStatuses: Store? = {
guard case .course = context.contextType else { return nil }
return env.subscribe(GetCustomGradeStatuses(courseID: context.id)) { [weak self] in
- self?.updateNavBar()
+ if #unavailable(iOS 26) {
+ self?.updateNavBar()
+ }
}
}()
lazy var colors = env.subscribe(GetCustomColors()) { [weak self] in
- self?.updateNavBar()
+ if #unavailable(iOS 26) {
+ self?.updateNavBar()
+ }
}
lazy var course = env.subscribe(GetCourse(courseID: context.id)) { [weak self] in
self?.updateNavBar()
@@ -76,7 +80,12 @@ public class PeopleListViewController: ScreenViewTrackableViewController, Colore
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .backgroundLightest
- setupTitleViewInNavbar(title: String(localized: "People", bundle: .core))
+
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "People", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "People", bundle: .core))
+ }
emptyMessageLabel.text = String(localized: "We couldn’t find somebody like that.", bundle: .core)
emptyTitleLabel.text = String(localized: "No Results", bundle: .core)
@@ -136,7 +145,11 @@ public class PeopleListViewController: ScreenViewTrackableViewController, Colore
}
spinnerView.color = color
refreshControl.color = color
- updateNavBar(subtitle: name, color: color)
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = name
+ } else {
+ updateNavBar(subtitle: name, color: color)
+ }
}
func update() {
diff --git a/Core/Core/Features/Planner/CalendarEvent/View/CalendarEventDetailsScreen.swift b/Core/Core/Features/Planner/CalendarEvent/View/CalendarEventDetailsScreen.swift
index b5c67d0b1d..28ccd80b9f 100644
--- a/Core/Core/Features/Planner/CalendarEvent/View/CalendarEventDetailsScreen.swift
+++ b/Core/Core/Features/Planner/CalendarEvent/View/CalendarEventDetailsScreen.swift
@@ -29,39 +29,78 @@ public struct CalendarEventDetailsScreen: View, ScreenViewTrackable {
}
public var body: some View {
- InstUI.BaseScreen(
- state: viewModel.state,
- refreshAction: viewModel.reload
- ) { _ in
- eventContent
- }
- .navigationBarTitleView(title: viewModel.pageTitle, subtitle: viewModel.pageSubtitle)
- .navBarItems(
- trailing: viewModel.shouldShowMenuButton
- ? InstUI.NavigationBarButton.moreIcon(
- isBackgroundContextColor: true,
- isEnabled: viewModel.isMoreButtonEnabled,
- isAvailableOffline: false,
- menuContent: {
- InstUI.MenuItem.edit { viewModel.didTapEdit.send(controller) }
- InstUI.MenuItem.delete { viewModel.didTapDelete.send(controller) }
+ if #available(iOS 26, *) {
+ InstUI.BaseScreen(
+ state: viewModel.state,
+ refreshAction: viewModel.reload
+ ) { _ in
+ eventContent
+ }
+ .navigationTitle(viewModel.pageTitle)
+ .optionalNavigationSubtitle(viewModel.pageSubtitle)
+ .toolbar {
+ if viewModel.shouldShowMenuButton {
+ InstUI.NavigationBarButton.moreIcon(
+ isEnabled: viewModel.isMoreButtonEnabled,
+ isAvailableOffline: false,
+ menuContent: {
+ InstUI.MenuItem.edit { viewModel.didTapEdit.send(controller) }
+ InstUI.MenuItem.delete { viewModel.didTapDelete.send(controller) }
+ }
+ )
+ .confirmation(
+ isPresented: $viewModel.shouldShowDeleteConfirmation,
+ presenting: viewModel.deleteConfirmation
+ )
}
+ }
+ .errorAlert(
+ isPresented: $viewModel.shouldShowDeleteError,
+ presenting: .init(
+ title: String(localized: "Deletion not completed", bundle: .core),
+ message: String(localized: "We couldn't delete your Event at this time. You can try it again.", bundle: .core),
+ buttonTitle: String(localized: "OK", bundle: .core)
+ )
)
.confirmation(
isPresented: $viewModel.shouldShowDeleteConfirmation,
presenting: viewModel.deleteConfirmation
)
- : nil
- )
- .navigationBarStyle(.color(viewModel.contextColor))
- .errorAlert(
- isPresented: $viewModel.shouldShowDeleteError,
- presenting: .init(
- title: String(localized: "Deletion not completed", bundle: .core),
- message: String(localized: "We couldn't delete your Event at this time. You can try it again.", bundle: .core),
- buttonTitle: String(localized: "OK", bundle: .core)
+ } else {
+ InstUI.BaseScreen(
+ state: viewModel.state,
+ refreshAction: viewModel.reload
+ ) { _ in
+ eventContent
+ }
+ .navigationBarTitleView(title: viewModel.pageTitle, subtitle: viewModel.pageSubtitle)
+ .navBarItems(
+ trailing: viewModel.shouldShowMenuButton
+ ? InstUI.NavigationBarButton.moreIcon(
+ isBackgroundContextColor: true,
+ isEnabled: viewModel.isMoreButtonEnabled,
+ isAvailableOffline: false,
+ menuContent: {
+ InstUI.MenuItem.edit { viewModel.didTapEdit.send(controller) }
+ InstUI.MenuItem.delete { viewModel.didTapDelete.send(controller) }
+ }
+ )
+ .confirmation(
+ isPresented: $viewModel.shouldShowDeleteConfirmation,
+ presenting: viewModel.deleteConfirmation
+ )
+ : nil
)
- )
+ .navigationBarStyle(.color(viewModel.contextColor))
+ .errorAlert(
+ isPresented: $viewModel.shouldShowDeleteError,
+ presenting: .init(
+ title: String(localized: "Deletion not completed", bundle: .core),
+ message: String(localized: "We couldn't delete your Event at this time. You can try it again.", bundle: .core),
+ buttonTitle: String(localized: "OK", bundle: .core)
+ )
+ )
+ }
}
private var eventContent: some View {
@@ -77,19 +116,21 @@ public struct CalendarEventDetailsScreen: View, ScreenViewTrackable {
#if DEBUG
#Preview {
- let event = CalendarEvent.save(
- .make(
- id: "",
- title: "Creative Machines and Innovative Instrumentation Conference",
- description: "We should meet 10 minutes before the event. Click here!",
- location_name: "UCF Department of Mechanical and Aerospace Engineering",
- location_address: "12760 Pegasus Dr\nOrlando, FL 32816"
- ),
- in: PreviewEnvironment().database.viewContext
- )
- let contextColor: UIColor = .red
+ NavigationStack {
+ let event = CalendarEvent.save(
+ .make(
+ id: "",
+ title: "Creative Machines and Innovative Instrumentation Conference",
+ description: "We should meet 10 minutes before the event. Click here!",
+ location_name: "UCF Department of Mechanical and Aerospace Engineering",
+ location_address: "12760 Pegasus Dr\nOrlando, FL 32816"
+ ),
+ in: PreviewEnvironment().database.viewContext
+ )
+ let contextColor: UIColor = .red
- return PlannerAssembly.makeEventDetailsScreenPreview(event: event, contextColor: contextColor)
+ return PlannerAssembly.makeEventDetailsScreenPreview(event: event, contextColor: contextColor)
+ }
}
#endif
diff --git a/Core/Core/Features/Planner/CalendarMain/PlannerViewController.swift b/Core/Core/Features/Planner/CalendarMain/PlannerViewController.swift
index 86e1e173ea..1954151d8f 100644
--- a/Core/Core/Features/Planner/CalendarMain/PlannerViewController.swift
+++ b/Core/Core/Features/Planner/CalendarMain/PlannerViewController.swift
@@ -68,7 +68,7 @@ public class PlannerViewController: VisibilityObservedViewController {
super.viewDidLoad()
view.backgroundColor = .backgroundLightest
- navigationItem.titleView = Brand.shared.headerImageView()
+ navigationItem.titleView = Brand.shared.headerImageView()
profileButton.accessibilityIdentifier = "PlannerCalendar.profileButton"
profileButton.accessibilityLabel = String(localized: "Profile Menu", bundle: .core)
diff --git a/Core/Core/Features/Planner/CalendarToDo/View/CalendarToDoDetailsScreen.swift b/Core/Core/Features/Planner/CalendarToDo/View/CalendarToDoDetailsScreen.swift
index d76fd6fc5e..485146597e 100644
--- a/Core/Core/Features/Planner/CalendarToDo/View/CalendarToDoDetailsScreen.swift
+++ b/Core/Core/Features/Planner/CalendarToDo/View/CalendarToDoDetailsScreen.swift
@@ -28,34 +28,55 @@ public struct CalendarToDoDetailsScreen: View {
}
public var body: some View {
- InstUI.BaseScreen(state: viewModel.state, config: viewModel.screenConfig) { _ in
- eventContent
- }
- .navigationBarTitleView(viewModel.navigationTitle)
- .navBarItems(
- trailing: .moreIcon(
- isBackgroundContextColor: true,
- isEnabled: viewModel.isMoreButtonEnabled,
- isAvailableOffline: false,
- menuContent: {
- InstUI.MenuItem.edit { viewModel.didTapEdit.send(controller) }
- InstUI.MenuItem.delete { viewModel.didTapDelete.send(controller) }
- }
- )
- )
- .navigationBarStyle(.color(viewModel.navBarColor))
- .confirmationAlert(
- isPresented: $viewModel.shouldShowDeleteConfirmation,
- presenting: viewModel.deleteConfirmationAlert
- )
- .errorAlert(
- isPresented: $viewModel.shouldShowDeleteError,
- presenting: .init(
- title: String(localized: "Deletion not completed", bundle: .core),
- message: String(localized: "We couldn't delete your To-do at this time. You can try it again.", bundle: .core),
- buttonTitle: String(localized: "OK", bundle: .core)
- )
- )
+ SwiftUI.Group {
+ if #available(iOS 26, *) {
+ InstUI.BaseScreen(state: viewModel.state, config: viewModel.screenConfig) { _ in
+ eventContent
+ }
+ .navigationTitle(viewModel.navigationTitle)
+ .toolbar {
+ ToolbarItem(placement: .topBarTrailing) {
+ InstUI.NavigationBarButton.moreIcon(
+ isEnabled: viewModel.isMoreButtonEnabled,
+ isAvailableOffline: false,
+ menuContent: {
+ InstUI.MenuItem.edit { viewModel.didTapEdit.send(controller) }
+ InstUI.MenuItem.delete { viewModel.didTapDelete.send(controller) }
+ }
+ )
+ }
+ }
+ } else {
+ InstUI.BaseScreen(state: viewModel.state, config: viewModel.screenConfig) { _ in
+ eventContent
+ }
+ .navigationBarTitleView(viewModel.navigationTitle)
+ .navBarItems(
+ trailing: .moreIcon(
+ isBackgroundContextColor: true,
+ isEnabled: viewModel.isMoreButtonEnabled,
+ isAvailableOffline: false,
+ menuContent: {
+ InstUI.MenuItem.edit { viewModel.didTapEdit.send(controller) }
+ InstUI.MenuItem.delete { viewModel.didTapDelete.send(controller) }
+ }
+ )
+ )
+ .navigationBarStyle(.color(viewModel.navBarColor))
+ }
+ }
+ .confirmationAlert(
+ isPresented: $viewModel.shouldShowDeleteConfirmation,
+ presenting: viewModel.deleteConfirmationAlert
+ )
+ .errorAlert(
+ isPresented: $viewModel.shouldShowDeleteError,
+ presenting: .init(
+ title: String(localized: "Deletion not completed", bundle: .core),
+ message: String(localized: "We couldn't delete your To Do at this time. You can try it again.", bundle: .core),
+ buttonTitle: String(localized: "OK", bundle: .core)
+ )
+ )
}
@ViewBuilder
diff --git a/Core/Core/Features/Quizzes/QuizDetails/Student/StudentQuizDetailsViewController.swift b/Core/Core/Features/Quizzes/QuizDetails/Student/StudentQuizDetailsViewController.swift
index 00fb78db2e..898ad4e879 100644
--- a/Core/Core/Features/Quizzes/QuizDetails/Student/StudentQuizDetailsViewController.swift
+++ b/Core/Core/Features/Quizzes/QuizDetails/Student/StudentQuizDetailsViewController.swift
@@ -77,7 +77,12 @@ public class StudentQuizDetailsViewController: ScreenViewTrackableViewController
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .backgroundLightest
- setupTitleViewInNavbar(title: String(localized: "Quiz Details", bundle: .core))
+
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Quiz Details", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Quiz Details", bundle: .core))
+ }
attemptsLabel.text = String(localized: "Allowed Attempts:", bundle: .core)
dueHeadingLabel.text = String(localized: "Due", bundle: .core)
@@ -118,7 +123,9 @@ public class StudentQuizDetailsViewController: ScreenViewTrackableViewController
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
- navigationController?.navigationBar.useContextColor(color)
+ if #unavailable(iOS 26) {
+ navigationController?.navigationBar.useContextColor(color)
+ }
}
@objc func refresh() {
@@ -131,8 +138,12 @@ public class StudentQuizDetailsViewController: ScreenViewTrackableViewController
func updateNavBar() {
guard let course = courses.first, !colors.pending else { return }
- updateNavBar(subtitle: course.name, color: course.color)
- view.tintColor = color
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = course.name
+ } else {
+ updateNavBar(subtitle: course.name, color: course.color)
+ }
+ view.tintColor = color
}
func update() {
diff --git a/Core/Core/Features/Quizzes/QuizListViewController.swift b/Core/Core/Features/Quizzes/QuizListViewController.swift
index c8e81d0a9f..feb9455980 100644
--- a/Core/Core/Features/Quizzes/QuizListViewController.swift
+++ b/Core/Core/Features/Quizzes/QuizListViewController.swift
@@ -56,7 +56,11 @@ public class QuizListViewController: ScreenViewTrackableViewController, ColoredN
public override func viewDidLoad() {
super.viewDidLoad()
- setupTitleViewInNavbar(title: String(localized: "Quizzes", bundle: .core))
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Quizzes", bundle: .core)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Quizzes", bundle: .core))
+ }
emptyMessageLabel.text = String(localized: "It looks like quizzes haven’t been created in this space yet.", bundle: .core)
emptyTitleLabel.text = String(localized: "No Quizzes", bundle: .core)
@@ -80,7 +84,9 @@ public class QuizListViewController: ScreenViewTrackableViewController, ColoredN
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.selectRow(at: nil, animated: false, scrollPosition: .none)
- navigationController?.navigationBar.useContextColor(color)
+ if #unavailable(iOS 26) {
+ navigationController?.navigationBar.useContextColor(color)
+ }
}
@objc func refresh() {
@@ -96,7 +102,11 @@ public class QuizListViewController: ScreenViewTrackableViewController, ColoredN
func update() {
if let course = course.first, colors.pending == false {
- updateNavBar(subtitle: course.name, color: course.color)
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = course.name
+ } else {
+ updateNavBar(subtitle: course.name, color: course.color)
+ }
view.tintColor = course.color
}
loadingView.isHidden = quizzes.state != .loading || refreshControl.isRefreshing
@@ -120,7 +130,11 @@ extension QuizListViewController: UITableViewDataSource, UITableViewDelegate {
public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let typeRaw = quizzes.sections?[section].name, let type = QuizType(rawValue: typeRaw) else { return nil }
- return SectionHeaderView.create(title: type.sectionTitle, section: section)
+ return if #available(iOS 26, *) {
+ SectionHeaderView.create(title: type.sectionTitle, section: section)
+ } else {
+ LegacySectionHeaderView.create(title: type.sectionTitle, section: section)
+ }
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
diff --git a/Core/Core/Features/Search/View/CoreSearchHostingController.swift b/Core/Core/Features/Search/View/CoreSearchHostingController.swift
index a5361b6256..7fb8de611d 100644
--- a/Core/Core/Features/Search/View/CoreSearchHostingController.swift
+++ b/Core/Core/Features/Search/View/CoreSearchHostingController.swift
@@ -69,7 +69,9 @@ public class CoreSearchHostingController<
}
)
).with {
- $0.tintColor = .textLightest
+ if #unavailable(iOS 26) {
+ $0.tintColor = .textLightest
+ }
$0.accessibilityLabel = String(localized: "Close", bundle: .core)
$0.accessibilityIdentifier = "close_bar_button"
}
@@ -82,7 +84,9 @@ public class CoreSearchHostingController<
}
)
).with {
- $0.tintColor = .textLightest
+ if #unavailable(iOS 26) {
+ $0.tintColor = .textLightest
+ }
$0.accessibilityIdentifier = "filter_bar_button"
$0.accessibilityLabel = String(localized: "Filter", bundle: .core)
}
@@ -100,16 +104,22 @@ public class CoreSearchHostingController<
)
)
.with {
- $0.tintColor = .textLightest
+ if #unavailable(iOS 26) {
+ $0.tintColor = .textLightest
+ }
$0.accessibilityIdentifier = "support_bar_button"
$0.accessibilityLabel = String(localized: "Help", bundle: .core)
}
}()
private lazy var searchFieldView: UISearchField = {
- let searchView = UISearchField(
- frame: CGRect(origin: .zero, size: CGSize(width: 400, height: 100))
- )
+ let frame = if #available(iOS 26, *) {
+ CGRect(origin: .zero, size: CGSize(width: 400, height: 56))
+ } else {
+ CGRect(origin: .zero, size: CGSize(width: 400, height: 100))
+ }
+
+ let searchView = UISearchField(frame: frame)
searchView.field.placeholder = searchContext.searchPrompt
searchView.field.accessibilityIdentifier = "ui_search_field"
searchView.field.delegate = self
@@ -140,7 +150,7 @@ public class CoreSearchHostingController<
super.init(SearchHostingBaseView(content: content, searchContext: searchContext))
- if let contextColor = attributes.accentColor {
+ if #unavailable(iOS 26), let contextColor = attributes.accentColor {
navigationBarStyle = .color(contextColor)
}
}
@@ -186,7 +196,9 @@ public class CoreSearchHostingController<
// Reset large enough length to re-fit search field in
// navigation title view
let length = view.bounds.width
- searchFieldView.frame.size = CGSize(width: length, height: length)
+ if #unavailable(iOS 26) {
+ searchFieldView.frame.size = CGSize(width: length, height: length)
+ }
navigationItem.leftBarButtonItems = [closeBarItem]
navigationItem.hidesBackButton = true
diff --git a/Core/Core/Features/Search/View/SearchContentContainerView.swift b/Core/Core/Features/Search/View/SearchContentContainerView.swift
index 81f0fc8c5a..e0d7c17a7a 100644
--- a/Core/Core/Features/Search/View/SearchContentContainerView.swift
+++ b/Core/Core/Features/Search/View/SearchContentContainerView.swift
@@ -73,7 +73,7 @@ struct SearchContentContainerView
+
+
diff --git a/Core/Core/Resources/Info.plist b/Core/Core/Resources/Info.plist
index 9f1cbe4274..1007fd9dd7 100644
--- a/Core/Core/Resources/Info.plist
+++ b/Core/Core/Resources/Info.plist
@@ -20,7 +20,5 @@
$(CURRENT_PROJECT_VERSION)
NSPrincipalClass
- UIDesignRequiresCompatibility
-
diff --git a/Core/Core/Resources/Localizable.xcstrings b/Core/Core/Resources/Localizable.xcstrings
index 3234fd8edd..e9338c1485 100644
--- a/Core/Core/Resources/Localizable.xcstrings
+++ b/Core/Core/Resources/Localizable.xcstrings
@@ -403233,7 +403233,6 @@
}
},
"We couldn't delete your To Do at this time. You can try it again." : {
- "extractionState" : "stale",
"localizations" : {
"ar" : {
"stringUnit" : {
@@ -403490,6 +403489,7 @@
}
},
"We couldn't delete your To-do at this time. You can try it again." : {
+ "extractionState" : "stale",
"localizations" : {
"ar" : {
"stringUnit" : {
diff --git a/Core/CoreTests/Common/CommonModels/Router/RouterTests.swift b/Core/CoreTests/Common/CommonModels/Router/RouterTests.swift
index 191584ed62..eb0017009a 100644
--- a/Core/CoreTests/Common/CommonModels/Router/RouterTests.swift
+++ b/Core/CoreTests/Common/CommonModels/Router/RouterTests.swift
@@ -203,7 +203,11 @@ class RouterTests: CoreTestCase {
XCTAssert(mockView.shown?.isKind(of: UIViewController.self) == true)
}
- func testDetailSplitViewButtons() {
+ func testDetailSplitViewButtons() throws {
+ if #available(iOS 26, *) {
+ throw XCTSkip("Default implementation provided above iOS 26")
+ }
+
let mockView = MockViewController()
mockView.navigationItem.leftItemsSupplementBackButton = false
mockView.navigationItem.leftBarButtonItems = nil
diff --git a/Core/CoreTests/Common/CommonUI/NavigationBar/UIKit/UINavigationBarExtensionsTests.swift b/Core/CoreTests/Common/CommonUI/NavigationBar/UIKit/UINavigationBarExtensionsTests.swift
index 8710517483..537121c145 100644
--- a/Core/CoreTests/Common/CommonUI/NavigationBar/UIKit/UINavigationBarExtensionsTests.swift
+++ b/Core/CoreTests/Common/CommonUI/NavigationBar/UIKit/UINavigationBarExtensionsTests.swift
@@ -21,16 +21,25 @@ import UIKit
@testable import Core
class UINavigationBarExtensionsTests: XCTestCase {
- func testUseContextColor() {
+ func testUseContextColor() throws {
+ if #available(iOS 26, *) {
+ throw XCTSkip("No toolbar colors above iOS 26")
+ }
+
let bar = UINavigationBar(frame: .zero)
bar.useContextColor(.backgroundDarkest)
+
XCTAssertEqual((bar.titleTextAttributes?[.foregroundColor] as? UIColor)?.hexString, UIColor.textLightest.variantForLightMode.hexString)
XCTAssertEqual(bar.tintColor.hexString, UIColor.textLightest.variantForLightMode.hexString)
XCTAssertEqual(bar.barTintColor?.hexString, UIColor.backgroundDarkest.hexString)
XCTAssertEqual(bar.barStyle, .black)
}
- func testUseGlobalNavStyle() {
+ func testUseGlobalNavStyle() throws {
+ if #available(iOS 26, *) {
+ throw XCTSkip("No toolbar colors above iOS 26")
+ }
+
let bar = UINavigationBar(frame: .zero)
bar.useGlobalNavStyle()
XCTAssertEqual((bar.titleTextAttributes?[.foregroundColor] as? UIColor)!.hexString, Brand.shared.navTextColor.hexString)
diff --git a/Core/CoreTests/Common/CommonUI/Presenter/ColoredNavViewProtocol.swift b/Core/CoreTests/Common/CommonUI/Presenter/ColoredNavViewProtocol.swift
index e9fecd8140..7f76454ee4 100644
--- a/Core/CoreTests/Common/CommonUI/Presenter/ColoredNavViewProtocol.swift
+++ b/Core/CoreTests/Common/CommonUI/Presenter/ColoredNavViewProtocol.swift
@@ -19,6 +19,7 @@
import XCTest
@testable import Core
+@available(iOS, deprecated: 26)
class ColoredNavViewProtocolTests: XCTestCase, ColoredNavViewProtocol {
var color: UIColor?
var navigationController: UINavigationController? = UINavigationController(rootViewController: UIViewController())
@@ -32,13 +33,21 @@ class ColoredNavViewProtocolTests: XCTestCase, ColoredNavViewProtocol {
titleSubtitleView = TitleSubtitleView.create()
}
- func testSetupTitleViewInNavbar() {
+ func testSetupTitleViewInNavbar() throws {
+ if #available(iOS 26, *) {
+ throw XCTSkip("ColoredNavViewProtocol is not used above iOS 26")
+ }
+
setupTitleViewInNavbar(title: self.name)
XCTAssertEqual(titleSubtitleView.title, self.name)
XCTAssertEqual(navigationItem.titleView, titleSubtitleView)
}
- func testUpdateNavBar() {
+ func testUpdateNavBar() throws {
+ if #available(iOS 26, *) {
+ throw XCTSkip("ColoredNavViewProtocol is not used above iOS 26")
+ }
+
let expectedColor: UIColor = .red.darkenToEnsureContrast(against: .textLightest.variantForLightMode)
updateNavBar(subtitle: subtitle, color: expectedColor)
diff --git a/Core/CoreTests/Common/CommonUI/UIViews/EmptyViewControllerTests.swift b/Core/CoreTests/Common/CommonUI/UIViews/EmptyViewControllerTests.swift
index 406aacd3d4..712124d94d 100644
--- a/Core/CoreTests/Common/CommonUI/UIViews/EmptyViewControllerTests.swift
+++ b/Core/CoreTests/Common/CommonUI/UIViews/EmptyViewControllerTests.swift
@@ -34,7 +34,9 @@ class EmptyViewControllerTests: CoreTestCase {
}
wait(for: [waitExpectation], timeout: 3.0)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, Brand.shared.navBackground.hexString)
+ if #unavailable(iOS 26) {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, Brand.shared.navBackground.hexString)
+ }
XCTAssertEqual(controller.view.subviews.count, 1)
XCTAssert(controller.view.subviews.first is UIImageView)
diff --git a/Core/CoreTests/Common/Extensions/UIKit/UIBarButtonItemExtensionsTests.swift b/Core/CoreTests/Common/Extensions/UIKit/UIBarButtonItemExtensionsTests.swift
index c965b76f14..41dcb651e8 100644
--- a/Core/CoreTests/Common/Extensions/UIKit/UIBarButtonItemExtensionsTests.swift
+++ b/Core/CoreTests/Common/Extensions/UIKit/UIBarButtonItemExtensionsTests.swift
@@ -22,8 +22,11 @@ import XCTest
class UIBarButtonItemExtensionsTests: XCTestCase {
func testBackButton() {
- let config = UIImage.SymbolConfiguration(weight: .semibold)
- let backImage = UIImage(systemName: "chevron.backward", withConfiguration: config)
+ let backImage = if #available(iOS 26, *) {
+ UIImage(systemName: "chevron.backward")
+ } else {
+ UIImage(systemName: "chevron.backward", withConfiguration: UIImage.SymbolConfiguration(weight: .semibold))
+ }
var actionCallsCount = 0
let testee = UIBarButtonItem.back { actionCallsCount += 1 }
diff --git a/Core/CoreTests/Common/Extensions/UIKit/UITableViewExtensionsTests.swift b/Core/CoreTests/Common/Extensions/UIKit/UITableViewExtensionsTests.swift
index 15b8ec19c5..dbacd2c346 100644
--- a/Core/CoreTests/Common/Extensions/UIKit/UITableViewExtensionsTests.swift
+++ b/Core/CoreTests/Common/Extensions/UIKit/UITableViewExtensionsTests.swift
@@ -35,6 +35,14 @@ class UITableViewExtensionsTests: XCTestCase {
XCTAssertNotNil(cell)
}
+ func testRegisterLegacyHeaderWithNib() {
+ let table = UITableView(frame: .zero)
+ table.registerHeaderFooterView(LegacySectionHeaderView.self)
+ let header: LegacySectionHeaderView = table.dequeueHeaderFooter(LegacySectionHeaderView.self)
+ XCTAssertNotNil(header)
+ }
+
+ @available(iOS 26, *)
func testRegisterHeaderWithNib() {
let table = UITableView(frame: .zero)
table.registerHeaderFooterView(SectionHeaderView.self)
diff --git a/Core/CoreTests/Features/Conferences/ConferenceDetailsViewControllerTests.swift b/Core/CoreTests/Features/Conferences/ConferenceDetailsViewControllerTests.swift
index 7e185349c3..3238109c92 100644
--- a/Core/CoreTests/Features/Conferences/ConferenceDetailsViewControllerTests.swift
+++ b/Core/CoreTests/Features/Conferences/ConferenceDetailsViewControllerTests.swift
@@ -55,9 +55,15 @@ class ConferenceDetailsViewControllerTests: CoreTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(controller.titleSubtitleView.title, "Conference Details")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
- XCTAssertEqual(nav.navigationBar.barTintColor!.hexString, UIColor(hexString: "#f00")!.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Conference Details")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Conference Details")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ XCTAssertEqual(nav.navigationBar.barTintColor!.hexString, UIColor(hexString: "#f00")!.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ }
XCTAssertEqual(controller.titleLabel.text, "Pandemic playthrough")
XCTAssertEqual(controller.statusLabel.text, "Not Started")
diff --git a/Core/CoreTests/Features/Conferences/ConferenceListViewControllerTests.swift b/Core/CoreTests/Features/Conferences/ConferenceListViewControllerTests.swift
index e5523ed568..7ea7492860 100644
--- a/Core/CoreTests/Features/Conferences/ConferenceListViewControllerTests.swift
+++ b/Core/CoreTests/Features/Conferences/ConferenceListViewControllerTests.swift
@@ -52,9 +52,14 @@ class ConferenceListViewControllerTests: CoreTestCase {
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(controller.titleSubtitleView.title, "Conferences")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
- XCTAssertEqual(nav.navigationBar.barTintColor!.hexString, UIColor(hexString: "#f00")!.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Conferences")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Conferences")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ XCTAssertEqual(nav.navigationBar.barTintColor!.hexString, UIColor(hexString: "#f00")!.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ }
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 0), 3)
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 1), 1)
diff --git a/Core/CoreTests/Features/CourseSync/CourseSyncDownloader/Model/Downloaders/CourseSyncAnnouncementsInteractorLiveTests.swift b/Core/CoreTests/Features/CourseSync/CourseSyncDownloader/Model/Downloaders/CourseSyncAnnouncementsInteractorLiveTests.swift
index 2575b3c58e..1174a514e3 100644
--- a/Core/CoreTests/Features/CourseSync/CourseSyncDownloader/Model/Downloaders/CourseSyncAnnouncementsInteractorLiveTests.swift
+++ b/Core/CoreTests/Features/CourseSync/CourseSyncDownloader/Model/Downloaders/CourseSyncAnnouncementsInteractorLiveTests.swift
@@ -45,7 +45,11 @@ class CourseSyncAnnouncementsInteractorLiveTests: CoreTestCase {
testee.viewWillAppear(false)
// MARK: - THEN
- XCTAssertEqual((testee.navigationItem.titleView as? TitleSubtitleView)?.subtitle, "Course One")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(testee.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual((testee.navigationItem.titleView as? TitleSubtitleView)?.subtitle, "Course One")
+ }
guard let announcementCell = testee.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? AnnouncementListCell else {
return XCTFail()
diff --git a/Core/CoreTests/Features/CourseSync/CourseSyncDownloader/Model/Downloaders/CourseSyncConferencesInteractorLiveTests.swift b/Core/CoreTests/Features/CourseSync/CourseSyncDownloader/Model/Downloaders/CourseSyncConferencesInteractorLiveTests.swift
index d34573f993..3d6c394810 100644
--- a/Core/CoreTests/Features/CourseSync/CourseSyncDownloader/Model/Downloaders/CourseSyncConferencesInteractorLiveTests.swift
+++ b/Core/CoreTests/Features/CourseSync/CourseSyncDownloader/Model/Downloaders/CourseSyncConferencesInteractorLiveTests.swift
@@ -31,7 +31,70 @@ class CourseSyncConferencesInteractorLiveTests: CoreTestCase {
XCTAssertEqual(CourseSyncConferencesInteractorLive(envResolver: envResolver).associatedTabType, .conferences)
}
- func testSavedDataPopulatesViewController() {
+ @available(iOS 26, *)
+ func testSavedDataPopulatesViewController() throws {
+ if #unavailable(iOS 26) {
+ throw XCTSkip("Skip below iOS 26")
+ }
+
+ // MARK: - GIVEN
+ api.mock(GetCustomColors(), value: .init(custom_colors: [:]))
+ api.mock(GetCourse(courseID: "testCourse"), value: .make(id: "testCourse"))
+ api.mock(GetConferences(context: .course("testCourse")), value: .init(conferences: [
+ .make(context_id: "course_testCourse",
+ description: "this test conference ended",
+ ended_at: .distantPast,
+ id: "1",
+ title: "ended conference"),
+ .make(context_id: "course_testCourse",
+ description: "this is an ongoing test conference",
+ id: "2",
+ started_at: Date().addingTimeInterval(-1),
+ title: "ongoing conference")
+ ]))
+ XCTAssertFinish(CourseSyncConferencesInteractorLive(envResolver: envResolver).getContent(courseId: "testCourse"))
+ API.resetMocks()
+
+ // MARK: - WHEN
+ OfflineModeAssembly.mock(AlwaysOfflineModeInteractor())
+ let testee = ConferenceListViewController.create(context: .course("testCourse"))
+ testee.view.layoutIfNeeded()
+ testee.viewWillAppear(false)
+ drainMainQueue()
+
+ // MARK: - THEN
+ XCTAssertEqual(testee.tableView.numberOfSections, 2) // new and concluded conferences sections
+
+ guard let newConferencesHeader = testee.tableView.headerView(forSection: 0) as? SectionHeaderView else {
+ return XCTFail()
+ }
+ XCTAssertEqual(newConferencesHeader.titleLabel.text, "New Conferences")
+
+ guard let concludedSectionHeader = testee.tableView.headerView(forSection: 1) as? SectionHeaderView else {
+ return XCTFail()
+ }
+ XCTAssertEqual(concludedSectionHeader.titleLabel.text, "Concluded Conferences")
+
+ guard let ongoingConferenceCell = testee.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? ConferenceListCell else {
+ return XCTFail()
+ }
+ XCTAssertEqual(ongoingConferenceCell.titleLabel.text, "ongoing conference")
+ XCTAssertEqual(ongoingConferenceCell.detailsLabel.text, "this is an ongoing test conference")
+ XCTAssertEqual(ongoingConferenceCell.statusLabel.text, "In Progress")
+
+ guard let concludedConferenceCell = testee.tableView.cellForRow(at: IndexPath(row: 0, section: 1)) as? ConferenceListCell else {
+ return XCTFail()
+ }
+ XCTAssertEqual(concludedConferenceCell.titleLabel.text, "ended conference")
+ XCTAssertEqual(concludedConferenceCell.detailsLabel.text, "this test conference ended")
+ XCTAssertEqual(concludedConferenceCell.statusLabel.text?.hasPrefix("Concluded"), true)
+ }
+
+ func testSavedDataPopulatesViewControllerWithLegacyHeaders() throws {
+ if #available(iOS 26, *) {
+ throw XCTSkip("Skip above iOS 26")
+ }
+
// MARK: - GIVEN
api.mock(GetCustomColors(), value: .init(custom_colors: [:]))
api.mock(GetCourse(courseID: "testCourse"), value: .make(id: "testCourse"))
@@ -60,12 +123,12 @@ class CourseSyncConferencesInteractorLiveTests: CoreTestCase {
// MARK: - THEN
XCTAssertEqual(testee.tableView.numberOfSections, 2) // new and concluded conferences sections
- guard let newConferencesHeader = testee.tableView.headerView(forSection: 0) as? SectionHeaderView else {
+ guard let newConferencesHeader = testee.tableView.headerView(forSection: 0) as? LegacySectionHeaderView else {
return XCTFail()
}
XCTAssertEqual(newConferencesHeader.titleLabel.text, "New Conferences")
- guard let concludedSectionHeader = testee.tableView.headerView(forSection: 1) as? SectionHeaderView else {
+ guard let concludedSectionHeader = testee.tableView.headerView(forSection: 1) as? LegacySectionHeaderView else {
return XCTFail()
}
XCTAssertEqual(concludedSectionHeader.titleLabel.text, "Concluded Conferences")
diff --git a/Core/CoreTests/Features/Courses/CourseDetails/ViewModel/CourseDetailsHeaderViewModelTests.swift b/Core/CoreTests/Features/Courses/CourseDetails/ViewModel/LegacyCourseDetailsHeaderViewModelTests.swift
similarity index 88%
rename from Core/CoreTests/Features/Courses/CourseDetails/ViewModel/CourseDetailsHeaderViewModelTests.swift
rename to Core/CoreTests/Features/Courses/CourseDetails/ViewModel/LegacyCourseDetailsHeaderViewModelTests.swift
index c9d6527a61..b8abfa4566 100644
--- a/Core/CoreTests/Features/Courses/CourseDetails/ViewModel/CourseDetailsHeaderViewModelTests.swift
+++ b/Core/CoreTests/Features/Courses/CourseDetails/ViewModel/LegacyCourseDetailsHeaderViewModelTests.swift
@@ -20,14 +20,15 @@
import XCTest
import TestsFoundation
-class CourseDetailsHeaderViewModelTests: CoreTestCase {
+@available(iOS, deprecated: 26, message: "Non-legacy version exists")
+class LegacyCourseDetailsHeaderViewModelTests: CoreTestCase {
func testProperties() {
api.mock(GetUserSettings(userID: "self"), value: .make(hide_dashcard_color_overlays: true))
let course = Course.save(.make(term: .make()), in: databaseClient)
course.contextColor = ContextColor.save(.init(custom_colors: ["1": "#FF0000"]), in: databaseClient)[0]
- let testee = CourseDetailsHeaderViewModel()
+ let testee = LegacyCourseDetailsHeaderViewModel()
testee.viewDidAppear()
testee.courseUpdated(course)
@@ -42,7 +43,7 @@ class CourseDetailsHeaderViewModelTests: CoreTestCase {
}
func testHeaderVisibility() {
- let testee = CourseDetailsHeaderViewModel()
+ let testee = LegacyCourseDetailsHeaderViewModel()
// header would take half of the screen's height
XCTAssertEqual(testee.shouldShowHeader(in: CGSize(width: 300, height: 2 * testee.height)), false)
// there's more space for cells than what the header blocks
@@ -50,7 +51,7 @@ class CourseDetailsHeaderViewModelTests: CoreTestCase {
}
func testPullToRefreshScrollCalculation() {
- let testee = CourseDetailsHeaderViewModel()
+ let testee = LegacyCourseDetailsHeaderViewModel()
// pull down as many points as high the header is
testee.scrollPositionChanged([.init(viewId: 0, bounds: .init(x: 0, y: testee.height, width: 0, height: 0))])
@@ -60,7 +61,7 @@ class CourseDetailsHeaderViewModelTests: CoreTestCase {
}
func testScrollUpToCellsCalculation() {
- let testee = CourseDetailsHeaderViewModel()
+ let testee = LegacyCourseDetailsHeaderViewModel()
// scroll up as many points as high the header is
let scrollOffset = -testee.height
testee.scrollPositionChanged([.init(viewId: 0, bounds: .init(x: 0, y: scrollOffset, width: 0, height: 0))])
diff --git a/Core/CoreTests/Features/Discussions/AnnouncementList/AnnouncementListViewControllerTests.swift b/Core/CoreTests/Features/Discussions/AnnouncementList/AnnouncementListViewControllerTests.swift
index 42126fb590..819f832349 100644
--- a/Core/CoreTests/Features/Discussions/AnnouncementList/AnnouncementListViewControllerTests.swift
+++ b/Core/CoreTests/Features/Discussions/AnnouncementList/AnnouncementListViewControllerTests.swift
@@ -81,9 +81,16 @@ class AnnouncementListViewControllerTests: CoreTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(controller.titleSubtitleView.title, "Announcements")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#0000ff")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Announcements")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Announcements")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#0000ff")
+ }
+
XCTAssertNil(controller.navigationItem.rightBarButtonItem)
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 0), 3)
@@ -142,9 +149,16 @@ class AnnouncementListViewControllerTests: CoreTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(controller.titleSubtitleView.title, "Announcements")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Group One")
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#000000")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Announcements")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Group One")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Announcements")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Group One")
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#000000")
+ }
+
XCTAssertNotNil(controller.navigationItem.rightBarButtonItem)
_ = controller.addButton.target?.perform(controller.addButton.action)
diff --git a/Core/CoreTests/Features/Discussions/DiscussionDetails/DiscussionDetailsViewControllerTests.swift b/Core/CoreTests/Features/Discussions/DiscussionDetails/DiscussionDetailsViewControllerTests.swift
index c3d9029f43..bda8ec2e1f 100644
--- a/Core/CoreTests/Features/Discussions/DiscussionDetails/DiscussionDetailsViewControllerTests.swift
+++ b/Core/CoreTests/Features/Discussions/DiscussionDetails/DiscussionDetailsViewControllerTests.swift
@@ -136,9 +136,16 @@ class DiscussionDetailsViewControllerTests: CoreTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#000088")
- XCTAssertEqual(controller.titleSubtitleView.title, "Discussion Details")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Discussion Details")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#000088")
+ XCTAssertEqual(controller.titleSubtitleView.title, "Discussion Details")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ }
+
XCTAssertEqual(controller.titleLabel.text, "What is a sandwich?")
XCTAssertEqual(controller.pointsLabel.text, "95 pts")
XCTAssertEqual(controller.pointsView.isHidden, false)
@@ -255,7 +262,11 @@ class DiscussionDetailsViewControllerTests: CoreTestCase {
controller.view.layoutIfNeeded()
XCTAssertEqual(controller.pointsView.isHidden, true)
XCTAssertEqual(controller.publishedView.isHidden, true)
- XCTAssertEqual(controller.titleSubtitleView.title, "Discussion Replies")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Discussion Replies")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Discussion Replies")
+ }
let html = getBodyHTML()
XCTAssert(!html.contains("Is the cube rule of food valid?"))
XCTAssert(html.contains("Why?"))
diff --git a/Core/CoreTests/Features/Discussions/DiscussionList/DiscussionListViewControllerTests.swift b/Core/CoreTests/Features/Discussions/DiscussionList/DiscussionListViewControllerTests.swift
index b7e7e66c7c..0bd21832c3 100644
--- a/Core/CoreTests/Features/Discussions/DiscussionList/DiscussionListViewControllerTests.swift
+++ b/Core/CoreTests/Features/Discussions/DiscussionList/DiscussionListViewControllerTests.swift
@@ -30,6 +30,7 @@ class DiscussionListViewControllerTests: CoreTestCase {
lazy var controller = DiscussionListViewController.create(context: .course("1"), env: environment)
+ // swiftlint:disable:next function_body_length
func testCourseDiscussions() {
api.mock(GetCourse(courseID: "1"), value: .make(enrollments: [
.make(
@@ -87,9 +88,14 @@ class DiscussionListViewControllerTests: CoreTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(controller.titleSubtitleView.title, "Discussions")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#0000ff")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Discussions")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Discussions")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#0000ff")
+ }
XCTAssertNil(controller.navigationItem.rightBarButtonItem)
XCTAssertEqual(controller.tableView.numberOfSections, 3)
@@ -229,9 +235,14 @@ class DiscussionListViewControllerTests: CoreTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(controller.titleSubtitleView.title, "Discussions")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Group One")
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#000000")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Discussions")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Group One")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Discussions")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Group One")
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#000000")
+ }
XCTAssertNotNil(controller.navigationItem.rightBarButtonItem)
_ = controller.addButton.target?.perform(controller.addButton.action)
diff --git a/Core/CoreTests/Features/Files/View/FileList/FileListViewControllerTests.swift b/Core/CoreTests/Features/Files/View/FileList/FileListViewControllerTests.swift
index 0ff745edc9..428bc84121 100644
--- a/Core/CoreTests/Features/Files/View/FileList/FileListViewControllerTests.swift
+++ b/Core/CoreTests/Features/Files/View/FileList/FileListViewControllerTests.swift
@@ -55,8 +55,14 @@ class FileListViewControllerTests: CoreTestCase {
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
drainMainQueue()
- XCTAssertEqual(controller.titleSubtitleView.title, "Folder A")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Folder A")
+ XCTAssertEqual(controller.navigationItem.subtitle, nil)
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Folder A")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "")
+ }
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 0), 0)
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 1), 0)
@@ -136,7 +142,12 @@ class FileListViewControllerTests: CoreTestCase {
index = IndexPath(row: 1, section: 2)
cell = controller.tableView.cellForRow(at: index) as? FileListCell
XCTAssertEqual(cell?.nameLabel.text, "Picture")
- XCTAssertEqual(controller.titleSubtitleView.title, "Folder Refresh")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Folder Refresh")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Folder Refresh")
+ }
XCTAssertEqual(controller.navigationItem.rightBarButtonItems?.contains(controller.editButton), true)
_ = controller.editButton.target?.perform(controller.editButton.action)
@@ -158,8 +169,13 @@ class FileListViewControllerTests: CoreTestCase {
controller.viewWillAppear(false)
drainMainQueue()
- XCTAssertEqual(controller.titleSubtitleView.title, "Files")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Files")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Files")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ }
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 0), 0)
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 1), 0)
@@ -178,8 +194,13 @@ class FileListViewControllerTests: CoreTestCase {
controller.viewWillAppear(false)
drainMainQueue()
- XCTAssertEqual(controller.titleSubtitleView.title, "Files")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Group One")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Files")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Group One")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Files")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Group One")
+ }
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 0), 0)
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 1), 0)
@@ -193,7 +214,11 @@ class FileListViewControllerTests: CoreTestCase {
controller.viewWillAppear(false)
drainMainQueue()
- XCTAssertEqual(controller.titleSubtitleView.title, "Folder A")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Folder A")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Folder A")
+ }
api.mock(controller.folder, value: [
.make(full_name: "my files/Folder Z", id: "2", name: "Folder Z", parent_folder_id: "1")
@@ -201,7 +226,12 @@ class FileListViewControllerTests: CoreTestCase {
controller.errorView.retryButton.sendActions(for: .primaryActionTriggered)
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 2), 2)
- XCTAssertEqual(controller.titleSubtitleView.title, "Folder Z")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Folder Z")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Folder Z")
+ }
+
XCTAssertEqual(controller.path, "Folder Z")
XCTAssertEqual(controller.folder.useCase.path, "Folder Z")
@@ -218,8 +248,12 @@ class FileListViewControllerTests: CoreTestCase {
controller.viewWillAppear(false)
drainMainQueue()
- XCTAssertEqual(controller.navigationItem.rightBarButtonItems?.contains(controller.addButton), true)
- _ = controller.addButton.target?.perform(controller.addButton.action)
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.rightBarButtonItems?.contains(controller.addButton), true)
+ } else {
+ XCTAssertEqual(controller.navigationItem.rightBarButtonItems?.contains(controller.legacyAddButton), true)
+ }
+ _ = controller.legacyAddButton.target?.perform(controller.legacyAddButton.action)
let sheet = router.presented as? BottomSheetPickerViewController
XCTAssertEqual(sheet?.actions.first?.title, "Add Folder")
sheet?.actions.first?.action()
@@ -246,8 +280,13 @@ class FileListViewControllerTests: CoreTestCase {
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(controller.navigationItem.rightBarButtonItems?.contains(controller.addButton), true)
- _ = controller.addButton.target?.perform(controller.addButton.action)
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.rightBarButtonItems?.contains(controller.addButton), true)
+ } else {
+ XCTAssertEqual(controller.navigationItem.rightBarButtonItems?.contains(controller.legacyAddButton), true)
+ }
+
+ _ = controller.legacyAddButton.target?.perform(controller.legacyAddButton.action)
let sheet = router.presented as? BottomSheetPickerViewController
XCTAssertEqual(sheet?.actions.last?.title, "Add File")
router.calls = []
@@ -291,7 +330,7 @@ class FileListViewControllerTests: CoreTestCase {
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- _ = controller.addButton.target?.perform(controller.addButton.action)
+ _ = controller.legacyAddButton.target?.perform(controller.legacyAddButton.action)
let sheet = router.presented as? BottomSheetPickerViewController
let titles = sheet?.actions.map { $0.title } ?? []
@@ -313,7 +352,7 @@ class FileListViewControllerTests: CoreTestCase {
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- _ = controller.addButton.target?.perform(controller.addButton.action)
+ _ = controller.legacyAddButton.target?.perform(controller.legacyAddButton.action)
let sheet = router.presented as? BottomSheetPickerViewController
let titles = sheet?.actions.map { $0.title } ?? []
XCTAssertFalse(titles.contains("Add File"), "Add File should be hidden when restricted")
diff --git a/Core/CoreTests/Features/LTI/View/LTIViewControllerTests.swift b/Core/CoreTests/Features/LTI/View/LTIViewControllerTests.swift
index 08fbf598fc..b186da8677 100644
--- a/Core/CoreTests/Features/LTI/View/LTIViewControllerTests.swift
+++ b/Core/CoreTests/Features/LTI/View/LTIViewControllerTests.swift
@@ -32,8 +32,14 @@ class LTIViewControllerTests: CoreTestCase {
controller.view.layoutIfNeeded()
XCTAssertEqual(controller.nameLabel.text, "LTI Tool")
XCTAssertTrue(controller.spinnerView.isHidden)
- XCTAssertEqual(controller.titleSubtitleView.title, "External Tool")
- XCTAssertNil(controller.titleSubtitleView.subtitle)
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "External Tool")
+ XCTAssertNil(controller.navigationItem.subtitle)
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "External Tool")
+ XCTAssertNil(controller.titleSubtitleView.subtitle)
+ }
task.resume()
XCTAssertEqual(controller.nameLabel.text, "So Descriptive")
@@ -61,7 +67,12 @@ class LTIViewControllerTests: CoreTestCase {
let controller = LTIViewController.create(env: environment, tools: tools)
api.mock(controller.courses!, value: course)
controller.view.layoutIfNeeded()
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Fancy Course")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.subtitle, "Fancy Course")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Fancy Course")
+ }
}
func testTextsWhenIsQuizLTI() {
diff --git a/Core/CoreTests/Features/Modules/ModuleItems/ModuleItemDetailsViewControllerTests.swift b/Core/CoreTests/Features/Modules/ModuleItems/ModuleItemDetailsViewControllerTests.swift
index 0f8714f6f7..52851028f1 100644
--- a/Core/CoreTests/Features/Modules/ModuleItems/ModuleItemDetailsViewControllerTests.swift
+++ b/Core/CoreTests/Features/Modules/ModuleItems/ModuleItemDetailsViewControllerTests.swift
@@ -51,8 +51,14 @@ class ModuleItemDetailsViewControllerTests: CoreTestCase {
XCTAssertTrue(controller.lockedView.isHidden)
XCTAssertFalse(controller.container.isHidden)
XCTAssertNotNil(controller.children.first as? FileDetailsViewController)
- XCTAssertEqual(controller.titleSubtitleView.title, "File Details")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "File Details")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "File Details")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ }
}
func testError() {
diff --git a/Core/CoreTests/Features/Modules/ModuleList/ModuleListViewControllerTests.swift b/Core/CoreTests/Features/Modules/ModuleList/ModuleListViewControllerTests.swift
index b6669c0892..6d59ae3231 100644
--- a/Core/CoreTests/Features/Modules/ModuleList/ModuleListViewControllerTests.swift
+++ b/Core/CoreTests/Features/Modules/ModuleList/ModuleListViewControllerTests.swift
@@ -126,11 +126,17 @@ class ModuleListViewControllerTests: CoreTestCase {
XCTAssertEqual(item4.completedStatusView.image, .checkLine)
XCTAssertNotNil(nav.viewControllers.first)
- XCTAssertEqual(viewController.titleSubtitleView.title, "Modules")
- XCTAssertEqual(viewController.titleSubtitleView.subtitle, "Course 1")
- XCTAssertEqual(
- viewController.navigationController?.navigationBar.barTintColor!.hexString,
- UIColor(hexString: "#fff")!.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(viewController.navigationItem.title, "Modules")
+ XCTAssertEqual(viewController.navigationItem.subtitle, "Course 1")
+ } else {
+ XCTAssertEqual(viewController.titleSubtitleView.title, "Modules")
+ XCTAssertEqual(viewController.titleSubtitleView.subtitle, "Course 1")
+ XCTAssertEqual(
+ viewController.navigationController?.navigationBar.barTintColor!.hexString,
+ UIColor(hexString: "#fff")!.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ }
}
func testLockedForUserDoesNotApplyToTeachers() {
diff --git a/Core/CoreTests/Features/Pages/PageDetails/PageDetailsViewControllerTests.swift b/Core/CoreTests/Features/Pages/PageDetails/PageDetailsViewControllerTests.swift
index fc287d05de..8126b73b16 100644
--- a/Core/CoreTests/Features/Pages/PageDetails/PageDetailsViewControllerTests.swift
+++ b/Core/CoreTests/Features/Pages/PageDetails/PageDetailsViewControllerTests.swift
@@ -52,9 +52,14 @@ class PageDetailsViewControllerTests: CoreTestCase {
controller.view.superview != nil
}
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#008800")
- XCTAssertEqual(controller.titleSubtitleView.title, "Test Page")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Test Page")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#008800")
+ XCTAssertEqual(controller.titleSubtitleView.title, "Test Page")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ }
XCTAssertEqual(controller.navigationItem.rightBarButtonItems?.count, 1)
let optionsButton = controller.navigationItem.rightBarButtonItem
@@ -94,7 +99,12 @@ class PageDetailsViewControllerTests: CoreTestCase {
XCTAssertEqual(controller.webView.scrollView.refreshControl?.isRefreshing, true)
RunLoop.main.run(until: Date() + 1.5)
XCTAssertEqual(controller.webView.scrollView.refreshControl?.isRefreshing, false)
- XCTAssertEqual(controller.titleSubtitleView.title, "Refreshed")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Refreshed")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Refreshed")
+ }
XCTAssertNil(controller.navigationItem.rightBarButtonItem)
}
@@ -109,9 +119,14 @@ class PageDetailsViewControllerTests: CoreTestCase {
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#000088")
- XCTAssertEqual(controller.titleSubtitleView.title, "Test Page")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Group One")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Test Page")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Group One")
+ } else {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#000088")
+ XCTAssertEqual(controller.titleSubtitleView.title, "Test Page")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Group One")
+ }
}
func testFrontPage() {
@@ -125,7 +140,12 @@ class PageDetailsViewControllerTests: CoreTestCase {
title: "Front Page"
))
controller.view.layoutIfNeeded()
- XCTAssertEqual(controller.titleSubtitleView.title, "Front Page")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Front Page")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "Front Page")
+ }
XCTAssertNil(controller.navigationItem.rightBarButtonItem)
}
}
diff --git a/Core/CoreTests/Features/Pages/PageList/PageListViewControllerTests.swift b/Core/CoreTests/Features/Pages/PageList/PageListViewControllerTests.swift
index 7918994d2c..73ea36ad10 100644
--- a/Core/CoreTests/Features/Pages/PageList/PageListViewControllerTests.swift
+++ b/Core/CoreTests/Features/Pages/PageList/PageListViewControllerTests.swift
@@ -43,9 +43,16 @@ class PageListViewControllerTests: CoreTestCase {
split.preferredDisplayMode = .oneBesideSecondary
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#000088")
- XCTAssertEqual(controller.titleSubtitleView.title, "Pages")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Pages")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#000088")
+ XCTAssertEqual(controller.titleSubtitleView.title, "Pages")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ }
+
XCTAssert(router.lastRoutedTo(.parse("/courses/42/pages/answers-page")))
let createButton = controller.navigationItem.rightBarButtonItem
@@ -109,9 +116,15 @@ class PageListViewControllerTests: CoreTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, UIColor(hexString: "#facade")!.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
- XCTAssertEqual(controller.titleSubtitleView.title, "Pages")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Group One")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Pages")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Group One")
+ } else {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, UIColor(hexString: "#facade")!.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ XCTAssertEqual(controller.titleSubtitleView.title, "Pages")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Group One")
+ }
XCTAssertNotNil(controller.navigationItem.rightBarButtonItem)
}
diff --git a/Core/CoreTests/Features/People/PeopleListViewControllerTests.swift b/Core/CoreTests/Features/People/PeopleListViewControllerTests.swift
index 1dd056b10c..98bf3db556 100644
--- a/Core/CoreTests/Features/People/PeopleListViewControllerTests.swift
+++ b/Core/CoreTests/Features/People/PeopleListViewControllerTests.swift
@@ -54,8 +54,13 @@ class PeopleListViewControllerTests: CoreTestCase {
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(controller.titleSubtitleView.title, "People")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "People")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(controller.titleSubtitleView.title, "People")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ }
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 0), 2)
diff --git a/Core/CoreTests/Features/Planner/CalendarMain/PlannerViewControllerTests.swift b/Core/CoreTests/Features/Planner/CalendarMain/PlannerViewControllerTests.swift
index 5375391eec..90cd53a690 100644
--- a/Core/CoreTests/Features/Planner/CalendarMain/PlannerViewControllerTests.swift
+++ b/Core/CoreTests/Features/Planner/CalendarMain/PlannerViewControllerTests.swift
@@ -74,7 +74,9 @@ class PlannerViewControllerTests: CoreTestCase {
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor!.hexString, Brand.shared.navBackground.hexString)
+ if #unavailable(iOS 26) {
+ XCTAssertEqual(nav.navigationBar.barTintColor!.hexString, Brand.shared.navBackground.hexString)
+ }
_ = controller.profileButton.target?.perform(controller.profileButton.action)
XCTAssert(router.lastRoutedTo("/profile", withOptions: .modal()))
diff --git a/Core/CoreTests/Features/Quizzes/QuizDetails/Student/StudentQuizDetailsViewControllerTests.swift b/Core/CoreTests/Features/Quizzes/QuizDetails/Student/StudentQuizDetailsViewControllerTests.swift
index e620c494d8..cc4d42266a 100644
--- a/Core/CoreTests/Features/Quizzes/QuizDetails/Student/StudentQuizDetailsViewControllerTests.swift
+++ b/Core/CoreTests/Features/Quizzes/QuizDetails/Student/StudentQuizDetailsViewControllerTests.swift
@@ -38,9 +38,16 @@ class StudentQuizDetailsViewControllerTests: CoreTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#0000ff")
- XCTAssertEqual(controller.titleSubtitleView.title, "Quiz Details")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Quiz Details")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#0000ff")
+ XCTAssertEqual(controller.titleSubtitleView.title, "Quiz Details")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+ }
+
XCTAssertEqual(controller.titleLabel.text, "What kind of pokemon are you?")
XCTAssertEqual(controller.pointsLabel.text, "11.1 pts")
XCTAssertEqual(controller.statusLabel.text, "Not Submitted")
diff --git a/Core/CoreTests/Features/Quizzes/QuizListViewControllerTests.swift b/Core/CoreTests/Features/Quizzes/QuizListViewControllerTests.swift
index 8ab2be5094..a186a22138 100644
--- a/Core/CoreTests/Features/Quizzes/QuizListViewControllerTests.swift
+++ b/Core/CoreTests/Features/Quizzes/QuizListViewControllerTests.swift
@@ -65,13 +65,17 @@ class QuizListViewControllerTests: CoreTestCase {
])
}
- func testLayout() {
- let nav = UINavigationController(rootViewController: controller)
+ @available(iOS 26, *)
+ func testLayout() throws {
+ if #unavailable(iOS 26) {
+ throw XCTSkip("Skip below iOS 26")
+ }
+
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#0000ff")
- XCTAssertEqual(controller.titleSubtitleView.title, "Quizzes")
- XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+
+ XCTAssertEqual(controller.navigationItem.title, "Quizzes")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
XCTAssertNoThrow(controller.viewWillDisappear(false))
@@ -124,6 +128,71 @@ class QuizListViewControllerTests: CoreTestCase {
XCTAssertEqual(controller.emptyTitleLabel.text, "No Quizzes")
XCTAssertEqual(controller.emptyMessageLabel.text, "It looks like quizzes haven’t been created in this space yet.")
}
+
+ func testLayoutWithLegacyHeaders() throws {
+ if #available(iOS 26, *) {
+ throw XCTSkip("Skip above iOS 26")
+ }
+
+ let nav = UINavigationController(rootViewController: controller)
+ controller.view.layoutIfNeeded()
+ controller.viewWillAppear(false)
+
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#0000ff")
+ XCTAssertEqual(controller.titleSubtitleView.title, "Quizzes")
+ XCTAssertEqual(controller.titleSubtitleView.subtitle, "Course One")
+
+ XCTAssertNoThrow(controller.viewWillDisappear(false))
+
+ var index = IndexPath(row: 0, section: 0)
+ var header = controller.tableView.headerView(forSection: index.section) as? LegacySectionHeaderView
+ XCTAssertEqual(header?.titleLabel.text, "Assignments")
+ var cell = controller.tableView.cellForRow(at: index) as? QuizListCell
+ XCTAssertEqual(cell?.titleLabel.text, "A")
+ XCTAssertEqual(cell?.dateLabel.text, "Due " + TestConstants.date0720.relativeDateTimeString)
+ XCTAssertEqual(cell?.pointsLabel.text, "111 pts")
+ XCTAssertEqual(cell?.questionsLabel.text, "111 Questions")
+ XCTAssertEqual(cell?.statusLabel.isHidden, true)
+ XCTAssertEqual(cell?.statusDot.isHidden, true)
+
+ index = IndexPath(row: 0, section: 1)
+ header = controller.tableView.headerView(forSection: index.section) as? LegacySectionHeaderView
+ XCTAssertEqual(header?.titleLabel.text, "Practice Quizzes")
+ cell = controller.tableView.cellForRow(at: index) as? QuizListCell
+ XCTAssertEqual(cell?.titleLabel.text, "B")
+ XCTAssertEqual(cell?.dateLabel.text, "Due " + TestConstants.date0315.relativeDateTimeString)
+ XCTAssertEqual(cell?.pointsLabel.text, "Not Graded")
+ XCTAssertEqual(cell?.statusLabel.text, "Closed")
+ XCTAssertEqual(cell?.statusDot.isHidden, false)
+
+ index = IndexPath(row: 0, section: 2)
+ header = controller.tableView.headerView(forSection: index.section) as? LegacySectionHeaderView
+ XCTAssertEqual(header?.titleLabel.text, "Graded Surveys")
+ cell = controller.tableView.cellForRow(at: index) as? QuizListCell
+ XCTAssertEqual(cell?.titleLabel.text, "C")
+ XCTAssertEqual(cell?.dateLabel.text, "No Due Date")
+
+ index = IndexPath(row: 0, section: 3)
+ header = controller.tableView.headerView(forSection: index.section) as? LegacySectionHeaderView
+ XCTAssertEqual(header?.titleLabel.text, "Surveys")
+ cell = controller.tableView.cellForRow(at: index) as? QuizListCell
+ XCTAssertEqual(cell?.titleLabel.text, "D")
+
+ controller.tableView.delegate?.tableView?(controller.tableView, didSelectRowAt: index)
+ XCTAssert(router.lastRoutedTo(URL(string: "/courses/1/quizzes/4")!))
+
+ api.mock(controller.quizzes, error: NSError.internalError())
+ controller.tableView.refreshControl?.sendActions(for: .primaryActionTriggered)
+ XCTAssertEqual(controller.errorView.isHidden, false)
+ XCTAssertEqual(controller.errorView.messageLabel.text, "There was an error loading quizzes. Pull to refresh to try again.")
+
+ api.mock(controller.quizzes, value: [])
+ controller.errorView.retryButton.sendActions(for: .primaryActionTriggered)
+ XCTAssertEqual(controller.errorView.isHidden, true)
+ XCTAssertEqual(controller.emptyView.isHidden, false)
+ XCTAssertEqual(controller.emptyTitleLabel.text, "No Quizzes")
+ XCTAssertEqual(controller.emptyMessageLabel.text, "It looks like quizzes haven’t been created in this space yet.")
+ }
}
class QuizListCellTests: CoreTestCase {
diff --git a/Core/CoreTests/Features/Syllabus/SyllabusTabViewControllerTests.swift b/Core/CoreTests/Features/Syllabus/SyllabusTabViewControllerTests.swift
index 525d9abe20..41aef1648e 100644
--- a/Core/CoreTests/Features/Syllabus/SyllabusTabViewControllerTests.swift
+++ b/Core/CoreTests/Features/Syllabus/SyllabusTabViewControllerTests.swift
@@ -35,10 +35,15 @@ class SyllabusTabViewControllerTests: CoreTestCase {
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#ed0000")
- let titleView = controller.navigationItem.titleView as? TitleSubtitleView
- XCTAssertEqual(titleView?.title, "Course Syllabus")
- XCTAssertEqual(titleView?.subtitle, "Course One")
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Course Syllabus")
+ XCTAssertEqual(controller.navigationItem.subtitle, "Course One")
+ } else {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, "#ed0000")
+ let titleView = controller.navigationItem.titleView as? TitleSubtitleView
+ XCTAssertEqual(titleView?.title, "Course Syllabus")
+ XCTAssertEqual(titleView?.subtitle, "Course One")
+ }
var cell = controller.collectionView(controller.menu!, cellForItemAt: IndexPath(item: 0, section: 0)) as? HorizontalMenuViewController.MenuCell
XCTAssertEqual(cell?.title?.text, "Syllabus")
diff --git a/Core/CoreTests/Features/Todos/TodoListViewControllerTests.swift b/Core/CoreTests/Features/Todos/TodoListViewControllerTests.swift
index ef6c894ec4..ad83b2e19a 100644
--- a/Core/CoreTests/Features/Todos/TodoListViewControllerTests.swift
+++ b/Core/CoreTests/Features/Todos/TodoListViewControllerTests.swift
@@ -45,7 +45,9 @@ class TodoListViewControllerTests: CoreTestCase {
let navigation = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(navigation.navigationBar.barTintColor!.hexString, Brand.shared.navBackground.hexString)
+ if #unavailable(iOS 26) {
+ XCTAssertEqual(navigation.navigationBar.barTintColor!.hexString, Brand.shared.navBackground.hexString)
+ }
XCTAssertEqual(controller.view.backgroundColor, .backgroundLightest)
XCTAssertEqual(controller.tableView.backgroundColor, .backgroundLightest)
XCTAssertNoThrow(controller.viewWillDisappear(false))
diff --git a/Parent/Parent/Courses/CourseDetailsViewController.swift b/Parent/Parent/Courses/CourseDetailsViewController.swift
index e28ea06a75..58c9f98386 100644
--- a/Parent/Parent/Courses/CourseDetailsViewController.swift
+++ b/Parent/Parent/Courses/CourseDetailsViewController.swift
@@ -76,8 +76,10 @@ class CourseDetailsViewController: HorizontalMenuViewController {
view.backgroundColor = .backgroundLightest
colorScheme = ColorScheme.observee(studentID)
navigationController?.setNavigationBarHidden(false, animated: true)
- navigationController?.navigationBar.useContextColor(colorScheme?.color)
- self.navigationItem.backBarButtonItem = UIBarButtonItem(title: String(localized: "Back", bundle: .parent), style: .plain, target: nil, action: nil)
+ if #unavailable(iOS 26) {
+ navigationController?.navigationBar.useContextColor(colorScheme?.color)
+ self.navigationItem.backBarButtonItem = UIBarButtonItem(title: String(localized: "Back", bundle: .parent), style: .plain, target: nil, action: nil)
+ }
delegate = self
customStatuses.refresh()
@@ -95,6 +97,19 @@ class CourseDetailsViewController: HorizontalMenuViewController {
courseReady()
}
+ override func viewWillDisappear(_ animated: Bool) {
+ if #available(iOS 26, *) {
+ // Remove Grade Screen's custom navigation bar color when navigating away
+ let appearance = UINavigationBarAppearance()
+ appearance.configureWithDefaultBackground()
+ appearance.backgroundColor = .backgroundLightest
+ navigationController?.navigationBar.standardAppearance = appearance
+ navigationController?.navigationBar.scrollEdgeAppearance = appearance
+ }
+
+ super.viewWillDisappear(animated)
+ }
+
override func setupPages() {
super.setupPages()
// This is to prevent the swipe to back gesture interfering with the collectionview horizontal scroll when on the first page
@@ -184,6 +199,15 @@ class CourseDetailsViewController: HorizontalMenuViewController {
}
}
+ if #available(iOS 26, *), itemCount <= 1 {
+ // Set the toolbar background if the tab switcher is not present to match the Grade Screen's header
+ let appearance = UINavigationBarAppearance()
+ appearance.configureWithDefaultBackground()
+ appearance.backgroundColor = .backgroundLight
+ navigationController?.navigationBar.standardAppearance = appearance
+ navigationController?.navigationBar.scrollEdgeAppearance = appearance
+ }
+
layoutViewControllers()
configureComposeMessageButton()
}
diff --git a/Parent/Parent/Dashboard/DashboardViewController.swift b/Parent/Parent/Dashboard/DashboardViewController.swift
index f7bbd8d9d2..9a87fb00a1 100644
--- a/Parent/Parent/Dashboard/DashboardViewController.swift
+++ b/Parent/Parent/Dashboard/DashboardViewController.swift
@@ -46,6 +46,7 @@ class DashboardViewController: ScreenViewTrackableViewController, ErrorViewContr
let screenViewTrackingParameters = ScreenViewTrackingParameters(eventName: "/")
let headerViewModel = StudentHeaderViewModel()
var subscriptions = Set()
+ var headerViewController: UIViewController?
lazy var addStudentController = AddStudentController(presentingViewController: self, handler: { [weak self] error in
if error == nil {
@@ -105,6 +106,9 @@ class DashboardViewController: ScreenViewTrackableViewController, ErrorViewContr
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
+ if #available(iOS 26, *) {
+ headerViewController?.view.alpha = 1
+ }
navigationController?.navigationBar.useContextColor(currentColor)
navigationController?.setNavigationBarHidden(true, animated: true)
updateBadge()
@@ -113,6 +117,12 @@ class DashboardViewController: ScreenViewTrackableViewController, ErrorViewContr
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
+ if #available(iOS 26, *) {
+ // The default navigation animation fades out the navigation bar, so we fade out our custom one
+ UIView.animate(withDuration: 0.2) {
+ self.headerViewController?.view.alpha = 0
+ }
+ }
navigationController?.setNavigationBarHidden(false, animated: true)
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
}
@@ -245,7 +255,13 @@ class DashboardViewController: ScreenViewTrackableViewController, ErrorViewContr
}
private func embedHeaderView() {
- let headerViewController = CoreHostingController(StudentHeaderView(viewModel: headerViewModel))
+ headerViewController = if #available(iOS 26, *) {
+ CoreHostingController(StudentHeaderView(viewModel: headerViewModel))
+ } else {
+ CoreHostingController(LegacyStudentHeaderView(viewModel: headerViewModel))
+ }
+ guard let headerViewController else { return }
+
embed(headerViewController, in: view) { [studentListView] header, superview in
let headerView = header.view!
headerView.translatesAutoresizingMaskIntoConstraints = false
diff --git a/Parent/Parent/Dashboard/View/LegacyStudentHeaderView.swift b/Parent/Parent/Dashboard/View/LegacyStudentHeaderView.swift
new file mode 100644
index 0000000000..ac23c3fe5e
--- /dev/null
+++ b/Parent/Parent/Dashboard/View/LegacyStudentHeaderView.swift
@@ -0,0 +1,200 @@
+//
+// This file is part of Canvas.
+// Copyright (C) 2025-present Instructure, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+//
+
+import Core
+import SwiftUI
+
+@available(iOS, deprecated: 26, message: "Non-legacy version exists")
+struct LegacyStudentHeaderView: View {
+ @ObservedObject private var viewModel: StudentHeaderViewModel
+ @Environment(\.viewController) private var controller
+ @Environment(\.dynamicTypeSize) private var dynamicTypeSize
+ @Environment(\.verticalSizeClass) private var verticalSizeClass
+ @ScaledMetric private var uiScale: CGFloat = 1
+ @AccessibilityFocusState private var isStudentViewFocused: Bool
+
+ private var isVerticallyCompact: Bool {
+ verticalSizeClass == .compact
+ }
+ private let horizontalPadding: CGFloat = 16
+ private var menuIconSize: CGFloat { uiScale.iconScale * 24 }
+ private var avatarSize: CGFloat { isVerticallyCompact ? 32 : 48 }
+ private var navBarHeight: CGFloat { isVerticallyCompact ? 42 : 91 }
+
+ init(viewModel: StudentHeaderViewModel) {
+ self.viewModel = viewModel
+ }
+
+ var body: some View {
+ HStack(alignment: isVerticallyCompact ? .center : .top, spacing: horizontalPadding) {
+ menuButton
+ studentView
+ Color.clear.frame(width: horizontalPadding + menuIconSize)
+ }
+ .frame(height: navBarHeight, alignment: .center)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .animation(nil, value: viewModel.backgroundColor)
+ .background(viewModel.backgroundColor)
+ .animation(.default, value: viewModel.backgroundColor)
+ .foregroundStyle(Color.textLightest)
+ }
+
+ private var studentView: some View {
+ Button {
+ viewModel.didTapStudentView.send(())
+ } label: {
+ VStack {
+ viewBody
+ .id(viewModel.state)
+ .transition(.push(from: .bottom))
+ .accessibilityElement()
+ .accessibilityAddTraits(.isHeader)
+ .accessibilityLabel(viewModel.accessibilityLabel)
+ .accessibilityValue(viewModel.accessibilityValue)
+ .accessibilityHint(viewModel.accessibilityHint)
+ }
+ // Match the animation we use for the student carousel appearance
+ .animation(.easeOut(duration: 0.3), value: viewModel.state)
+ .clipped()
+ }
+ .frame(maxWidth: .infinity, alignment: .center)
+ .accessibilityFocused($isStudentViewFocused)
+ .onReceive(viewModel.focusStudentPicker) {
+ isStudentViewFocused = true
+ }
+ }
+
+ @ViewBuilder
+ private var viewBody: some View {
+ if isVerticallyCompact {
+ HStack(spacing: 8) {
+ icon
+ label
+ }
+ } else {
+ VStack(spacing: 8) {
+ icon
+ label
+ }
+ }
+ }
+
+ @ViewBuilder
+ private var icon: some View {
+ switch viewModel.state {
+ case .addStudent:
+ Circle()
+ .frame(width: avatarSize, height: avatarSize)
+ .overlay {
+ Image.addLine
+ .size(avatarSize / 2)
+ .foregroundStyle(viewModel.backgroundColor)
+ }
+ .dropShadow()
+ case .student(let name, let avatarURL):
+ Avatar(name: name, url: avatarURL, size: avatarSize)
+ .dropShadow()
+ }
+ }
+
+ @ViewBuilder
+ private var label: some View {
+ switch viewModel.state {
+ case .addStudent:
+ addStudentLabel
+ case .student(let name, _):
+ studentNameWithDropDown(name: name)
+ }
+ }
+
+ private func studentNameWithDropDown(name: String) -> some View {
+ HStack(spacing: 4) {
+ Text(name)
+ .font(.semibold16)
+ .lineLimit(1)
+ Image.dropdown
+ .size(uiScale.iconScale * 12)
+ .rotationEffect(.degrees(viewModel.isDropdownClosed ? 0 : -180))
+ }
+ }
+
+ private var addStudentLabel: some View {
+ Text("Add Student", bundle: .parent)
+ .font(.semibold16)
+ }
+
+ private var menuButton: some View {
+ Button {
+ viewModel.didTapMenuButton.send(controller.value)
+ } label: {
+ Image.hamburgerSolid
+ .resizable()
+ .size(menuIconSize)
+ .foregroundColor(Color.textLightest)
+ .instBadge(viewModel.badgeCount)
+ }
+ .padding(.leading, horizontalPadding)
+ .padding(.top, isVerticallyCompact ? 0 : 12)
+ .identifier("Dashboard.profileButton")
+ .accessibilityLabel(Text("Profile Menu", bundle: .parent))
+ .accessibilityValue(String(localized: "Closed", bundle: .core))
+ .accessibilityHint(viewModel.menuAccessibilityHint)
+ }
+}
+
+private extension View {
+
+ func dropShadow() -> some View {
+ shadow(
+ color: .black.opacity(0.12),
+ radius: 8,
+ x: 0,
+ y: 4
+ )
+ }
+}
+
+#if DEBUG
+
+#Preview {
+ VStack {
+ LegacyStudentHeaderView(viewModel: StudentHeaderViewModel())
+ Spacer()
+ }
+}
+
+#Preview {
+ let previewEnvironment = PreviewEnvironment()
+ let user = User.save(
+ // swiftlint:disable:next line_length
+ .make(short_name: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."),
+ in: previewEnvironment.database.viewContext
+ )
+ let viewModel = {
+ let model = StudentHeaderViewModel()
+ model.didSelectStudent.send(user)
+ return model
+ }()
+
+ VStack {
+ LegacyStudentHeaderView(viewModel: viewModel)
+ Spacer()
+ }
+}
+
+#endif
diff --git a/Parent/Parent/Dashboard/View/StudentHeaderView.swift b/Parent/Parent/Dashboard/View/StudentHeaderView.swift
index 0af281183b..7989608627 100644
--- a/Parent/Parent/Dashboard/View/StudentHeaderView.swift
+++ b/Parent/Parent/Dashboard/View/StudentHeaderView.swift
@@ -16,9 +16,12 @@
// along with this program. If not, see .
//
+import Foundation
+
import Core
import SwiftUI
+@available(iOS, introduced: 26, message: "Legacy version exists")
struct StudentHeaderView: View {
@ObservedObject private var viewModel: StudentHeaderViewModel
@Environment(\.viewController) private var controller
@@ -33,24 +36,17 @@ struct StudentHeaderView: View {
private let horizontalPadding: CGFloat = 16
private var menuIconSize: CGFloat { uiScale.iconScale * 24 }
private var avatarSize: CGFloat { isVerticallyCompact ? 32 : 48 }
- private var navBarHeight: CGFloat { isVerticallyCompact ? 42 : 91 }
+ private var navBarHeight: CGFloat { isVerticallyCompact ? 56 : 94 }
init(viewModel: StudentHeaderViewModel) {
self.viewModel = viewModel
}
var body: some View {
- HStack(alignment: isVerticallyCompact ? .center : .top, spacing: horizontalPadding) {
- menuButton
- studentView
- Color.clear.frame(width: horizontalPadding + menuIconSize)
- }
- .frame(height: navBarHeight, alignment: .center)
- .frame(maxWidth: .infinity, alignment: .leading)
- .animation(nil, value: viewModel.backgroundColor)
- .background(viewModel.backgroundColor)
- .animation(.default, value: viewModel.backgroundColor)
- .foregroundStyle(Color.textLightest)
+ studentView
+ .frame(height: navBarHeight, alignment: .center)
+ .frame(maxWidth: .infinity)
+ .background(.backgroundLightest)
}
private var studentView: some View {
@@ -69,27 +65,35 @@ struct StudentHeaderView: View {
}
// Match the animation we use for the student carousel appearance
.animation(.easeOut(duration: 0.3), value: viewModel.state)
- .clipped()
}
- .frame(maxWidth: .infinity, alignment: .center)
.accessibilityFocused($isStudentViewFocused)
.onReceive(viewModel.focusStudentPicker) {
isStudentViewFocused = true
}
+ .buttonStyle(.plain)
}
@ViewBuilder
private var viewBody: some View {
if isVerticallyCompact {
HStack(spacing: 8) {
+ menuButton
+ Spacer()
icon
label
+ Spacer()
}
} else {
- VStack(spacing: 8) {
+ VStack(spacing: 4) {
+ Spacer()
icon
label
}
+ .frame(maxWidth: .infinity, alignment: .center)
+ .overlay(alignment: .topLeading) {
+ menuButton
+ }
+ .padding(.bottom, 30)
}
}
@@ -99,8 +103,10 @@ struct StudentHeaderView: View {
case .addStudent:
Circle()
.frame(width: avatarSize, height: avatarSize)
+ .foregroundStyle(.clear)
+ .glassEffect()
.overlay {
- Image.addLine
+ Image.addSolid
.size(avatarSize / 2)
.foregroundStyle(viewModel.backgroundColor)
}
@@ -113,12 +119,18 @@ struct StudentHeaderView: View {
@ViewBuilder
private var label: some View {
- switch viewModel.state {
- case .addStudent:
- addStudentLabel
- case .student(let name, _):
- studentNameWithDropDown(name: name)
+ Group {
+ switch viewModel.state {
+ case .addStudent:
+ addStudentLabel
+ case .student(let name, _):
+ studentNameWithDropDown(name: name)
+ }
}
+ .padding(.vertical, 8)
+ .padding(.horizontal, 12)
+ .glassEffect()
+ .padding(.horizontal, isVerticallyCompact ? 0 : 64)
}
private func studentNameWithDropDown(name: String) -> some View {
@@ -144,9 +156,12 @@ struct StudentHeaderView: View {
Image.hamburgerSolid
.resizable()
.size(menuIconSize)
- .foregroundColor(Color.textLightest)
- .instBadge(viewModel.badgeCount)
+ .foregroundColor(Color.textDarkest)
+ // Could not find a better way to make the button a circle
+ .frame(height: 34)
}
+ .instBadge(viewModel.badgeCount)
+ .buttonStyle(.glass)
.padding(.leading, horizontalPadding)
.padding(.top, isVerticallyCompact ? 0 : 12)
.identifier("Dashboard.profileButton")
@@ -170,14 +185,16 @@ private extension View {
#if DEBUG
-#Preview {
+#Preview("Add student") {
VStack {
- StudentHeaderView(viewModel: StudentHeaderViewModel())
+ if #available(iOS 26, *) {
+ StudentHeaderView(viewModel: StudentHeaderViewModel())
+ }
Spacer()
}
}
-#Preview {
+#Preview("Student Name") {
let previewEnvironment = PreviewEnvironment()
let user = User.save(
// swiftlint:disable:next line_length
@@ -191,7 +208,29 @@ private extension View {
}()
VStack {
- StudentHeaderView(viewModel: viewModel)
+ if #available(iOS 26, *) {
+ StudentHeaderView(viewModel: viewModel)
+ }
+ Spacer()
+ }
+}
+
+#Preview("Beyoncé") {
+ let previewEnvironment = PreviewEnvironment()
+ let user = User.save(
+ .make(short_name: "Beyoncé"),
+ in: previewEnvironment.database.viewContext
+ )
+ let viewModel = {
+ let model = StudentHeaderViewModel()
+ model.didSelectStudent.send(user)
+ return model
+ }()
+
+ VStack {
+ if #available(iOS 26, *) {
+ StudentHeaderView(viewModel: viewModel)
+ }
Spacer()
}
}
diff --git a/Parent/Parent/Info.plist b/Parent/Parent/Info.plist
index 883a68db82..ee05a3fd5d 100644
--- a/Parent/Parent/Info.plist
+++ b/Parent/Parent/Info.plist
@@ -94,8 +94,6 @@
Frameworks/Core.framework/lato_bolditalic.ttf
Frameworks/Core.framework/lato_regular.ttf
- UIDesignRequiresCompatibility
-
UILaunchStoryboardName
LaunchScreen
UIRequiredDeviceCapabilities
diff --git a/Parent/Parent/Planner/CalendarEventDetailsViewController.swift b/Parent/Parent/Planner/CalendarEventDetailsViewController.swift
index 3c3d3764d5..1a37d5f2b4 100644
--- a/Parent/Parent/Planner/CalendarEventDetailsViewController.swift
+++ b/Parent/Parent/Planner/CalendarEventDetailsViewController.swift
@@ -70,7 +70,13 @@ class CalendarEventDetailsViewController: UIViewController, ColoredNavViewProtoc
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .backgroundLightest
- setupTitleViewInNavbar(title: String(localized: "Event Details", bundle: .parent))
+
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Event Details", bundle: .parent)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Event Details", bundle: .parent))
+ }
+
updateNavBar(subtitle: nil, color: ColorScheme.observee(studentID).color)
webViewContainer.addSubview(webView)
webView.pinWithThemeSwitchButton(inside: webViewContainer)
@@ -130,7 +136,11 @@ class CalendarEventDetailsViewController: UIViewController, ColoredNavViewProtoc
func update() {
guard let event = events.first else { return }
if let title = event.contextName {
- setupTitleViewInNavbar(title: title)
+ if #available(iOS 26, *) {
+ navigationItem.title = title
+ } else {
+ setupTitleViewInNavbar(title: title)
+ }
}
titleLabel.text = event.title
diff --git a/Parent/ParentUnitTests/AccountNotifications/AccountNotificationDetailsViewControllerTests.swift b/Parent/ParentUnitTests/AccountNotifications/AccountNotificationDetailsViewControllerTests.swift
index be8e18d0a2..78308cc41e 100644
--- a/Parent/ParentUnitTests/AccountNotifications/AccountNotificationDetailsViewControllerTests.swift
+++ b/Parent/ParentUnitTests/AccountNotifications/AccountNotificationDetailsViewControllerTests.swift
@@ -36,7 +36,10 @@ class AccountNotificationDetailsViewControllerTests: ParentTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.hexString)
+
+ if #unavailable(iOS 26) {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.hexString)
+ }
XCTAssertEqual(controller.title, "Announcement")
XCTAssertEqual(controller.titleLabel.text, "Pandemic")
diff --git a/Parent/ParentUnitTests/Assignments/View/ParentAssignmentDetailsViewControllerTests.swift b/Parent/ParentUnitTests/Assignments/View/ParentAssignmentDetailsViewControllerTests.swift
index cd42e55204..5200ef1498 100644
--- a/Parent/ParentUnitTests/Assignments/View/ParentAssignmentDetailsViewControllerTests.swift
+++ b/Parent/ParentUnitTests/Assignments/View/ParentAssignmentDetailsViewControllerTests.swift
@@ -44,7 +44,9 @@ class ParentAssignmentDetailsViewControllerTests: ParentTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.hexString)
+ if #unavailable(iOS 26) {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.hexString)
+ }
XCTAssertEqual(controller.title, "Course One")
XCTAssertEqual(controller.titleLabel.text, "some assignment")
XCTAssertEqual(controller.dateLabel.text, dueAt.dateTimeString)
diff --git a/Parent/ParentUnitTests/Discussions/DiscussionDetailsViewControllerTests.swift b/Parent/ParentUnitTests/Discussions/DiscussionDetailsViewControllerTests.swift
index 86aff0e0bb..fcdbc33000 100644
--- a/Parent/ParentUnitTests/Discussions/DiscussionDetailsViewControllerTests.swift
+++ b/Parent/ParentUnitTests/Discussions/DiscussionDetailsViewControllerTests.swift
@@ -37,7 +37,9 @@ class DiscussionDetailsViewControllerTests: ParentTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ if #unavailable(iOS 26) {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ }
XCTAssertEqual(controller.title, "Course One")
XCTAssertEqual(controller.titleLabel.text, "Pandemic")
diff --git a/Parent/ParentUnitTests/ObserverAlerts/ObserverAlertListViewControllerTests.swift b/Parent/ParentUnitTests/ObserverAlerts/ObserverAlertListViewControllerTests.swift
index a714e8f880..14cd03dd97 100644
--- a/Parent/ParentUnitTests/ObserverAlerts/ObserverAlertListViewControllerTests.swift
+++ b/Parent/ParentUnitTests/ObserverAlerts/ObserverAlertListViewControllerTests.swift
@@ -87,7 +87,9 @@ class ObserverAlertListViewControllerTests: ParentTestCase {
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
drainMainQueue()
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ if #unavailable(iOS 26) {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ }
XCTAssertEqual(controller.tableView.numberOfRows(inSection: 0), 4)
diff --git a/Parent/ParentUnitTests/Planner/CalendarEventDetailsViewControllerTests.swift b/Parent/ParentUnitTests/Planner/CalendarEventDetailsViewControllerTests.swift
index ceb63db1cc..6b71f4d874 100644
--- a/Parent/ParentUnitTests/Planner/CalendarEventDetailsViewControllerTests.swift
+++ b/Parent/ParentUnitTests/Planner/CalendarEventDetailsViewControllerTests.swift
@@ -54,8 +54,14 @@ class CalendarEventDetailsViewControllerTests: ParentTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.hexString)
- XCTAssertEqual(controller.titleSubtitleView.title, "Course One")
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Course One")
+ } else {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.hexString)
+ XCTAssertEqual(controller.titleSubtitleView.title, "Course One")
+ }
+
XCTAssertEqual(controller.titleLabel.text, "It's happening")
XCTAssertEqual(controller.dateLabel.text, TestConstants.date10.dateOnlyString)
XCTAssertEqual(controller.locationView.isHidden, false)
diff --git a/Parent/ParentUnitTests/Students/StudentDetailsViewControllerTests.swift b/Parent/ParentUnitTests/Students/StudentDetailsViewControllerTests.swift
index 541fc6a2b8..15ac3f9a1e 100644
--- a/Parent/ParentUnitTests/Students/StudentDetailsViewControllerTests.swift
+++ b/Parent/ParentUnitTests/Students/StudentDetailsViewControllerTests.swift
@@ -37,7 +37,9 @@ class StudentDetailsViewControllerTests: ParentTestCase {
let nav = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ if #unavailable(iOS 26) {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observee("1").color.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ }
XCTAssertEqual(controller.nameLabel.text, "Legion (They/Them)")
for (index, type) in AlertThresholdType.allCases.enumerated() {
diff --git a/Parent/ParentUnitTests/Students/StudentListViewControllerTests.swift b/Parent/ParentUnitTests/Students/StudentListViewControllerTests.swift
index 13dbec6003..f556f7f413 100644
--- a/Parent/ParentUnitTests/Students/StudentListViewControllerTests.swift
+++ b/Parent/ParentUnitTests/Students/StudentListViewControllerTests.swift
@@ -37,7 +37,9 @@ class StudentListViewControllerTests: ParentTestCase {
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observeeBlue.color.hexString)
+ if #unavailable(iOS 26) {
+ XCTAssertEqual(nav.navigationBar.barTintColor?.hexString, ColorScheme.observeeBlue.color.hexString)
+ }
XCTAssertEqual(controller.navigationItem.rightBarButtonItem?.action, #selector(controller.addStudentController.addStudent))
let index0 = IndexPath(row: 0, section: 0)
diff --git a/Student/Student/Assignments/AssignmentDetails/StudentAssignmentDetailsViewController.storyboard b/Student/Student/Assignments/AssignmentDetails/StudentAssignmentDetailsViewController.storyboard
index b6f5307b16..1cfe1302f7 100644
--- a/Student/Student/Assignments/AssignmentDetails/StudentAssignmentDetailsViewController.storyboard
+++ b/Student/Student/Assignments/AssignmentDetails/StudentAssignmentDetailsViewController.storyboard
@@ -1,8 +1,8 @@
-
+
-
+
@@ -679,10 +679,10 @@
diff --git a/Student/Student/Submissions/SubmissionDetails/SubmissionDetailsViewController.swift b/Student/Student/Submissions/SubmissionDetails/SubmissionDetailsViewController.swift
index 86ddc82b96..8b961c6e50 100644
--- a/Student/Student/Submissions/SubmissionDetails/SubmissionDetailsViewController.swift
+++ b/Student/Student/Submissions/SubmissionDetails/SubmissionDetailsViewController.swift
@@ -61,7 +61,11 @@ class SubmissionDetailsViewController: ScreenViewTrackableViewController, Submis
super.viewDidLoad()
view.backgroundColor = .backgroundLightest
- setupTitleViewInNavbar(title: String(localized: "Submission", bundle: .student))
+ if #available(iOS 26, *) {
+ navigationItem.title = String(localized: "Submission", bundle: .student)
+ } else {
+ setupTitleViewInNavbar(title: String(localized: "Submission", bundle: .student))
+ }
drawer?.selectionColor = env.flatMap({ context?.color(in: $0.database.viewContext) })
drawer?.tabs?.addTarget(self, action: #selector(drawerTabChanged), for: .valueChanged)
emptyView?.submitCallback = { [weak self] button in
@@ -156,7 +160,11 @@ class SubmissionDetailsViewController: ScreenViewTrackableViewController, Submis
guard let assignment = presenter?.assignment.first, let course = presenter?.course.first else {
return
}
- updateNavBar(subtitle: assignment.name, color: course.color)
+ if #available(iOS 26, *) {
+ navigationItem.subtitle = assignment.name
+ } else {
+ updateNavBar(subtitle: assignment.name, color: course.color)
+ }
view.tintColor = course.color
}
diff --git a/Student/StudentUnitTests/Assignments/AssignmentDetails/StudentAssignmentDetailsViewControllerTests.swift b/Student/StudentUnitTests/Assignments/AssignmentDetails/StudentAssignmentDetailsViewControllerTests.swift
index d31c138d99..2bb1892fbc 100644
--- a/Student/StudentUnitTests/Assignments/AssignmentDetails/StudentAssignmentDetailsViewControllerTests.swift
+++ b/Student/StudentUnitTests/Assignments/AssignmentDetails/StudentAssignmentDetailsViewControllerTests.swift
@@ -82,7 +82,10 @@ class StudentAssignmentDetailsViewControllerTests: StudentTestCase {
viewController.updateNavBar(subtitle: "hello", backgroundColor: .red)
XCTAssertEqual(viewController.titleSubtitleView.subtitle, "hello")
- XCTAssertEqual(viewController.navigationController?.navigationBar.barTintColor?.hexString, UIColor.red.hexString)
+
+ if #unavailable(iOS 26) {
+ XCTAssertEqual(viewController.navigationController?.navigationBar.barTintColor?.hexString, UIColor.red.hexString)
+ }
}
func testShowSubmitAssignmentButton() {
diff --git a/Student/StudentUnitTests/Groups/GroupNavigation/GroupNavigationViewControllerTests.swift b/Student/StudentUnitTests/Groups/GroupNavigation/GroupNavigationViewControllerTests.swift
index 6ad5423ed9..a942ee0526 100644
--- a/Student/StudentUnitTests/Groups/GroupNavigation/GroupNavigationViewControllerTests.swift
+++ b/Student/StudentUnitTests/Groups/GroupNavigation/GroupNavigationViewControllerTests.swift
@@ -40,9 +40,14 @@ class GroupNavigationViewControllerTests: StudentTestCase {
let navigation = UINavigationController(rootViewController: controller)
controller.view.layoutIfNeeded()
controller.viewWillAppear(false)
- XCTAssertEqual(navigation.navigationBar.barTintColor?.hexString, UIColor(hexString: "#f00")?.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+
+ if #available(iOS 26, *) {
+ XCTAssertEqual(controller.navigationItem.title, "Tests")
+ } else {
+ XCTAssertEqual(navigation.navigationBar.barTintColor?.hexString, UIColor(hexString: "#f00")?.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString)
+ XCTAssertEqual(controller.titleSubtitleView.title, "Tests")
+ }
XCTAssertEqual(controller.tableView.backgroundColor, .backgroundLightest)
- XCTAssertEqual(controller.titleSubtitleView.title, "Tests")
}
func testSelect() {
diff --git a/Teacher/Teacher/Assignments/AssignmentDetails/Views/TeacherAssignmentDetailsScreen.swift b/Teacher/Teacher/Assignments/AssignmentDetails/Views/TeacherAssignmentDetailsScreen.swift
index 93953d30db..3d1ae5022f 100644
--- a/Teacher/Teacher/Assignments/AssignmentDetails/Views/TeacherAssignmentDetailsScreen.swift
+++ b/Teacher/Teacher/Assignments/AssignmentDetails/Views/TeacherAssignmentDetailsScreen.swift
@@ -50,18 +50,30 @@ public struct TeacherAssignmentDetailsScreen: View, ScreenViewTrackable {
}
public var body: some View {
- states
- .background(Color.backgroundLightest)
- .navigationBarTitleView(
- title: String(localized: "Assignment Details", bundle: .teacher),
- subtitle: course.first?.name
- )
- .rightBarButtonItems(rightBarItems)
- .navigationBarStyle(.color(course.first?.color))
- .onAppear {
- refreshAssignments()
- refreshCourses()
- }
+ if #available(iOS 26, *) {
+ states
+ .background(Color.backgroundLightest)
+ .navigationTitle(.init("Assignment Details", bundle: .teacher))
+ .optionalNavigationSubtitle(course.first?.name)
+ .rightBarButtonItems(rightBarItems)
+ .onAppear {
+ refreshAssignments()
+ refreshCourses()
+ }
+ } else {
+ states
+ .background(Color.backgroundLightest)
+ .navigationBarTitleView(
+ title: String(localized: "Assignment Details", bundle: .teacher),
+ subtitle: course.first?.name
+ )
+ .rightBarButtonItems(rightBarItems)
+ .navigationBarStyle(.color(course.first?.color))
+ .onAppear {
+ refreshAssignments()
+ refreshCourses()
+ }
+ }
}
@ViewBuilder var states: some View {
diff --git a/Teacher/Teacher/Info.plist b/Teacher/Teacher/Info.plist
index 18566a73e7..fcc1b3fd5a 100644
--- a/Teacher/Teacher/Info.plist
+++ b/Teacher/Teacher/Info.plist
@@ -100,8 +100,6 @@
Frameworks/Core.framework/lato_bold.ttf
Frameworks/Core.framework/lato_regular.ttf
- UIDesignRequiresCompatibility
-
UILaunchStoryboardName
LaunchScreen
UIRequiredDeviceCapabilities
diff --git a/Teacher/Teacher/SpeedGrader/SpeedGraderScreen/View/SpeedGraderScreen.swift b/Teacher/Teacher/SpeedGrader/SpeedGraderScreen/View/SpeedGraderScreen.swift
index 8cec0c24fe..932318a3b9 100644
--- a/Teacher/Teacher/SpeedGrader/SpeedGraderScreen/View/SpeedGraderScreen.swift
+++ b/Teacher/Teacher/SpeedGrader/SpeedGraderScreen/View/SpeedGraderScreen.swift
@@ -47,30 +47,58 @@ struct SpeedGraderScreen: View, ScreenViewTrackable {
}
var body: some View {
- InstUI.BaseScreen(state: viewModel.state, config: screenConfig) { proxy in
- PagesViewControllerWrapper(
- dataSource: viewModel,
- delegate: viewModel,
- onViewControllerCreate: {
- viewModel.didShowPagesViewController.send($0)
+ if #available(iOS 26, *) {
+ InstUI.BaseScreen(state: viewModel.state, config: screenConfig) { proxy in
+ PagesViewControllerWrapper(
+ dataSource: viewModel,
+ delegate: viewModel,
+ onViewControllerCreate: {
+ viewModel.didShowPagesViewController.send($0)
+ }
+ )
+ .frame(width: proxy.size.width, height: proxy.size.height)
+ }
+ .navigationTitle(viewModel.navigationTitle)
+ .navigationSubtitle(viewModel.navigationSubtitle)
+ .toolbar {
+ if viewModel.isPostPolicyButtonVisible {
+ postPolicySettingsButton
}
+ doneButton
+ }
+ .onFirstAppear {
+ // When speedgrader is opened from a discussion
+ // the router automatically adds a done button
+ controller.value.navigationItem.leadingItemGroups = []
+ }
+ } else {
+ InstUI.BaseScreen(state: viewModel.state, config: screenConfig) { proxy in
+ PagesViewControllerWrapper(
+ dataSource: viewModel,
+ delegate: viewModel,
+ onViewControllerCreate: {
+ viewModel.didShowPagesViewController.send($0)
+ }
+ )
+ .frame(width: proxy.size.width, height: proxy.size.height)
+ }
+ .navigationBarTitleView(
+ title: viewModel.navigationTitle,
+ subtitle: viewModel.navigationSubtitle
)
- .frame(width: proxy.size.width, height: proxy.size.height)
- }
- .navigationBarTitleView(
- title: viewModel.navigationTitle,
- subtitle: viewModel.navigationSubtitle
- )
- .navBarItems(trailing: navBarTrailingItems)
- .navigationBarStyle(.color(viewModel.navigationBarColor))
- .onFirstAppear {
- setupStatusBarStyleUpdates()
- // When speedgrader is opened from a discussion
- // the router automatically adds a done button
- controller.value.navigationItem.leadingItemGroups = []
+ .navBarItems(trailing: navBarTrailingItems)
+ .navigationBarStyle(.color(viewModel.navigationBarColor))
+ .onFirstAppear {
+ setupStatusBarStyleUpdates()
+ // When speedgrader is opened from a discussion
+ // the router automatically adds a done button
+ controller.value.navigationItem.leadingItemGroups = []
+ }
}
}
+ @available(iOS, deprecated: 26, message: "Toolbars are not colored above iOS 26")
+ // Sets the status bar color for the colored toolbar
private func setupStatusBarStyleUpdates() {
guard let controller = controller.value as? CoreHostingController else {
return
@@ -92,8 +120,9 @@ struct SpeedGraderScreen: View, ScreenViewTrackable {
}
private var doneButton: InstUI.NavigationBarButton {
- .done(
- isBackgroundContextColor: true,
+ let isBackgroundContextColor = if #available(iOS 26, *) { false } else { true }
+ return .done(
+ isBackgroundContextColor: isBackgroundContextColor,
accessibilityId: "SpeedGrader.doneButton"
) {
viewModel.didTapDoneButton.send(controller)
@@ -106,7 +135,7 @@ struct SpeedGraderScreen: View, ScreenViewTrackable {
} label: {
Image.eyeLine
.size(24 * uiScale.iconScale)
- .foregroundColor(.textLightest)
+ .foregroundStyleBelow26(.textLightest)
}
.identifier("SpeedGrader.postPolicyButton")
.accessibilityLabel(Text("Post settings", bundle: .teacher))
diff --git a/Teacher/Teacher/Submissions/SubmissionsList/View/SubmissionListScreen.swift b/Teacher/Teacher/Submissions/SubmissionsList/View/SubmissionListScreen.swift
index ebfabb620b..4e83160069 100644
--- a/Teacher/Teacher/Submissions/SubmissionsList/View/SubmissionListScreen.swift
+++ b/Teacher/Teacher/Submissions/SubmissionsList/View/SubmissionListScreen.swift
@@ -32,16 +32,28 @@ struct SubmissionListScreen: View {
}
var body: some View {
- InstUI.BaseScreen(
- state: viewModel.state,
- refreshAction: { completion in
- viewModel.refresh(completion)
- },
- content: { _ in listView }
- )
- .toolbar(content: { toolbarContent })
- .navigationTitle(Text("Submissions", bundle: .teacher))
- .navigationBarStyle(.color(viewModel.course?.color))
+ if #available(iOS 26, *) {
+ InstUI.BaseScreen(
+ state: viewModel.state,
+ refreshAction: { completion in
+ viewModel.refresh(completion)
+ },
+ content: { _ in listView }
+ )
+ .toolbar { toolbarContent }
+ .navigationTitle(Text("Submissions", bundle: .teacher))
+ } else {
+ InstUI.BaseScreen(
+ state: viewModel.state,
+ refreshAction: { completion in
+ viewModel.refresh(completion)
+ },
+ content: { _ in listView }
+ )
+ .toolbar { toolbarContent }
+ .navigationTitle(Text("Submissions", bundle: .teacher))
+ .navigationBarStyle(.color(viewModel.course?.color))
+ }
}
private var listView: some View {
@@ -93,24 +105,35 @@ struct SubmissionListScreen: View {
private var toolbarContent: some ToolbarContent {
ToolbarItemGroup(placement: .topBarTrailing) {
-
- InstUI
- .NavigationBarButton
- .filterIcon(
- isBackgroundContextColor: true,
- isSolid: viewModel.isFilterActive,
- action: {
- viewModel.showFilterScreen(from: controller)
- }
- )
- .tint(Color.textLightest)
+ if #available(iOS 26, *) {
+ InstUI
+ .NavigationBarButton
+ .filterIcon(
+ isSolid: viewModel.isFilterActive,
+ action: {
+ viewModel.showFilterScreen(from: controller)
+ }
+ )
+ .tint(Color.textLightest)
+ } else {
+ InstUI
+ .NavigationBarButton
+ .filterIcon(
+ isBackgroundContextColor: true,
+ isSolid: viewModel.isFilterActive,
+ action: {
+ viewModel.showFilterScreen(from: controller)
+ }
+ )
+ .tint(Color.textLightest)
+ }
Button {
viewModel.openPostPolicy(from: controller)
} label: {
Image.eyeLine
}
- .tint(Color.textLightest)
+ .tintBelow26(Color.textLightest)
.accessibilityLabel(Text("Post settings", bundle: .teacher))
.accessibilityIdentifier("SubmissionsList.postPolicyButton")
@@ -119,7 +142,7 @@ struct SubmissionListScreen: View {
} label: {
Image.emailLine
}
- .tint(Color.textLightest)
+ .tintBelow26(Color.textLightest)
.accessibility(label: Text("Send message to users", bundle: .teacher))
}
}
diff --git a/Teacher/TeacherUnitTests/Attendance/AttendanceViewControllerTests.swift b/Teacher/TeacherUnitTests/Attendance/AttendanceViewControllerTests.swift
index b134a54835..b818ea6828 100644
--- a/Teacher/TeacherUnitTests/Attendance/AttendanceViewControllerTests.swift
+++ b/Teacher/TeacherUnitTests/Attendance/AttendanceViewControllerTests.swift
@@ -67,10 +67,14 @@ class AttendanceViewControllerTests: TeacherTestCase {
func testStatusDisplay() {
loadView()
- XCTAssertEqual(
- controller.navigationController?.navigationBar.barTintColor?.hexString,
- UIColor(hexString: courseColor)!.variantForLightMode.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString
- )
+
+ if #unavailable(iOS 26) {
+ XCTAssertEqual(
+ controller.navigationController?.navigationBar.barTintColor?.hexString,
+ UIColor(hexString: courseColor)!.variantForLightMode.darkenToEnsureContrast(against: .textLightest.variantForLightMode).hexString
+ )
+ }
+
XCTAssertEqual(controller.view.backgroundColor, .backgroundLightest)
XCTAssertEqual(controller.tableView.refreshControl?.isRefreshing, true)
RunLoop.main.run(until: Date() + 1)