Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Navigation bar titles #3129

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public struct EmbeddedWebPageContainerScreen: View {
viewModel.viewController = viewController.value
}
}
.navigationTitle(viewModel.navTitle, subtitle: viewModel.subTitle)
.navigationBarTitleView(title: viewModel.navTitle, subtitle: viewModel.subTitle)
.navigationBarStyle(.color(viewModel.contextColor))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// 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 <https://www.gnu.org/licenses/>.
//

import SwiftUI

extension InstUI {

public struct NavigationBarTitleView: View {
@Environment(\.dynamicTypeSize) private var dynamicTypeSize
@Environment(\.navBarColors) private var navBarColors

private let title: String
private let subtitle: String?

public init(
title: String,
subtitle: String? = nil
) {
self.title = title
self.subtitle = subtitle
}

public var body: some View {
VStack(spacing: 1) {
Text(title)
.font(.semibold16)
.foregroundColor(navBarColors.title)

if let subtitle, subtitle.isNotEmpty {
Text(subtitle)
.font(.regular14)
.foregroundColor(navBarColors.subtitle)
}
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isHeader)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// 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 <https://www.gnu.org/licenses/>.
//

import SwiftUI

struct NavigationBarColorConfiguration {
var title: Color
var subtitle: Color
var tint: Color

// private, because currently we don't want to allow custom configurations
private init(title: Color, subtitle: Color, tint: Color) {
self.title = title
self.subtitle = subtitle
self.tint = tint
}

init(style: NavigationBarStyle, brand: Brand = .shared) {
switch style {
case .modal:
self.init(
title: .textDarkest,
subtitle: .textDark,
tint: brand.linkColor.asColor
)
case .global:
let color = brand.navTextColor.asColor
self.init(title: color, subtitle: color, tint: color)
case .color:
let color = Color.textLightest
self.init(title: color, subtitle: color, tint: color)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// This file is part of Canvas.
// Copyright (C) 2024-present Instructure, Inc.
// 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
Expand All @@ -16,25 +16,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//

import Foundation
import Combine
import UIKit

protocol AttachmentPickerInteractor {
var files: PassthroughSubject<[File], Error> { get }
var alreadyUploadedFiles: CurrentValueSubject<[File], Never> { get }
var isCancelConfirmationNeeded: Bool { get }

func uploadFiles()

func addFile(url: URL)

func addFile(file: File)

func retry()

func cancel()

func removeFile(file: File)

func deleteFile(file: File) -> AnyPublisher<Void, Never>
public enum NavigationBarStyle: Equatable {
case modal
case global
case color(UIColor?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,40 +19,20 @@
import SwiftUI

protocol NavigationBarStyled: AnyObject {
var navigationBarStyle: UINavigationBar.Style { get set }
var navigationBarStyle: NavigationBarStyle { get set }
}

struct NavigationBarStyleModifier: ViewModifier {
let style: UINavigationBar.Style
let style: NavigationBarStyle

@Environment(\.viewController) var controller

func body(content: Content) -> some View {
(controller.value as? NavigationBarStyled)?.navigationBarStyle = style
controller.value.navigationController?.navigationBar.useStyle(style)
return content.overlay(Color?.none) // needs something modified to actually run
}
}

struct TitleSubtitleModifier: ViewModifier {
let title: String
let subtitle: String?

@Environment(\.viewController) var controller

func body(content: Content) -> some View {
let view = controller.value.navigationItem.titleView as? TitleSubtitleView ?? {
let view = TitleSubtitleView.create()
controller.value.navigationItem.titleView = view
return view
}()
view.title = title
view.subtitle = subtitle
var combinedTitle = title
if let subtitle = subtitle, subtitle != "" {
combinedTitle += ", \(subtitle)"
}
return content.navigationBarTitle(Text(combinedTitle))
return content.overlay(Color?.none) // needs something modified to actually run
.environment(\.navBarColors, .init(style: style))
}
}

Expand Down Expand Up @@ -87,14 +67,39 @@ struct NavBarBackButtonModifier: ViewModifier {
}

extension View {
public func navigationBarStyle(_ style: UINavigationBar.Style) -> some View {
/// 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:
/// - style:
/// - `.global` is used only on a few screens, typically on root screens of each tab.
/// - `.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.
public func navigationBarStyle(_ style: NavigationBarStyle) -> some View {
modifier(NavigationBarStyleModifier(style: style))
}

public func navigationTitle(_ title: String, subtitle: String?) -> some View {
modifier(TitleSubtitleModifier(title: title, subtitle: subtitle))
/// Sets the navigation bar's title and subtitle, using the proper fonts and arrangement.
/// - Warning: Make sure to call `navigationBarStyle()` _**AFTER**_ this method to set the proper text colors.
/// - 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.
public func navigationBarTitleView(title: String, subtitle: String?) -> some View {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is considered as being an implicit dependency and can be easily missed out.
Rather than dictating that a specific modifier must be called after this one for it work properly.
I would go with passing style as a required parameter to this modifier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it's not ideal, and I've considered passing style here, but it creates it's own problems.
Style sets the background color as well, and as a next step it's planned to govern the NavigationBarItems' color (via navBarColors.tint). That would mean we would have to set the style in 3 separate places which is also not ideal, plus it allows for mixed styles which we want to restrict.

I consider this solution as a lesser evil. And since order already matters for SwiftUI modifiers, it's a known concept (although maybe in an unexpected way, yes). Another mitigation of the issue is that this solution (and only this) would be used all over the project, with plenty of examples and documentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I conveyed my idea correctly. What I mean with passing style, as simple as having style modifier inside this one.

    public func navigationBarTitleView(title: String, subtitle: String?, style: NavigationBarStyle) -> some View {
        toolbar {
            ToolbarItem(placement: .principal) {
                InstUI.NavigationBarTitleView(title: title, subtitle: subtitle)
            }
        }
        .navigationBarStyle(style)
    }

You mean even doing that can create problems ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, basically I aim to avoid this:

content
    .navigationBarTitleView(title: ..., style: .modal)
    .navBarItems(trailing: ..., style: .modal)
    .navigationBarStyle(.modal)

or even:

content
    .navigationBarTitleView(title: ..., style: .modal)
    .navBarItems(trailing: ..., style: .modal) // which would call .navigationBarStyle() inside again in your example

toolbar {
ToolbarItem(placement: .principal) {
InstUI.NavigationBarTitleView(title: title, subtitle: subtitle)
}
}
}

/// 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.
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.
public func navigationBarGlobal() -> some View {
modifier(GlobalNavigationBarModifier())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public class TitleSubtitleView: UIView {
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]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
Expand All @@ -13,11 +13,11 @@
<rect key="frame" x="0.0" y="0.0" width="170" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="500" axis="vertical" distribution="fillProportionally" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="bdR-j1-wKj">
<stackView opaque="NO" contentMode="scaleToFill" horizontalCompressionResistancePriority="500" axis="vertical" distribution="fillProportionally" alignment="center" spacing="1" translatesAutoresizingMaskIntoConstraints="NO" id="bdR-j1-wKj">
<rect key="frame" x="0.0" y="0.0" width="170" height="32"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Stb-Qj-5cD" userLabel="titleLabel" customClass="DynamicLabel" customModule="Core" customModuleProvider="target">
<rect key="frame" x="67.5" y="0.0" width="35" height="18.5"/>
<rect key="frame" x="67.5" y="0.0" width="35" height="18"/>
<accessibility key="accessibilityConfiguration" identifier="NavBar.title">
<accessibilityTraits key="traits" staticText="YES" header="YES"/>
</accessibility>
Expand All @@ -29,7 +29,7 @@
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kd2-Sy-FDG" userLabel="subtitleLabel" customClass="DynamicLabel" customModule="Core" customModuleProvider="target">
<rect key="frame" x="63" y="18.5" width="44" height="13.5"/>
<rect key="frame" x="63" y="19" width="44" height="13"/>
<accessibility key="accessibilityConfiguration" identifier="NavBar.subtitle"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
Expand Down Expand Up @@ -63,12 +63,4 @@
<point key="canvasLocation" x="121" y="-264"/>
</view>
</objects>
<designables>
<designable name="Stb-Qj-5cD">
<size key="intrinsicContentSize" width="35" height="20.5"/>
</designable>
<designable name="kd2-Sy-FDG">
<size key="intrinsicContentSize" width="44" height="14.5"/>
</designable>
</designables>
</document>
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@
import UIKit

extension UINavigationBar {
public enum Style: Equatable { case modal, modalLight, global, color(UIColor?) }

public typealias Style = NavigationBarStyle

func useStyle(_ style: Style) {
switch style {
case .modal:
useModalStyle()
case .modalLight:
useModalStyle(isLightFont: true)
case .global:
useGlobalNavStyle()
case .color(let color):
Expand Down Expand Up @@ -65,7 +64,6 @@ extension UINavigationBar {
*/
public func useModalStyle(
brand: Brand = Brand.shared,
isLightFont: Bool = false,
forcedTheme: UITraitCollection? = nil
) {
var backgroundColor = UIColor.backgroundLightest
Expand All @@ -83,32 +81,28 @@ extension UINavigationBar {
barStyle = .default
isTranslucent = false

applyAppearanceChanges(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
isLightFont: isLightFont
)
applyAppearanceChanges(backgroundColor: backgroundColor, foregroundColor: foregroundColor)
}

private func applyAppearanceChanges(backgroundColor: UIColor?, foregroundColor: UIColor?, isLightFont: Bool = false) {
private func applyAppearanceChanges(backgroundColor: UIColor?, foregroundColor: UIColor?) {
let appearance = UINavigationBarAppearance()

if isTranslucent {
appearance.configureWithTransparentBackground()
} else {
appearance.configureWithDefaultBackground()

if let backgroundColor = backgroundColor {
if let backgroundColor {
appearance.backgroundColor = backgroundColor
}
}

if let foreGroundColor = foregroundColor {
appearance.titleTextAttributes = [.foregroundColor: foreGroundColor]
if let foregroundColor {
appearance.titleTextAttributes = [.foregroundColor: foregroundColor]
}

appearance.titleTextAttributes[.font] = UIFont.scaledNamedFont(isLightFont ? .semibold16 : .bold17)
appearance.buttonAppearance.normal.titleTextAttributes[.font] = UIFont.scaledNamedFont(isLightFont ? .semibold16 : .regular17)
appearance.titleTextAttributes[.font] = UIFont.scaledNamedFont(.semibold16)
appearance.buttonAppearance.normal.titleTextAttributes[.font] = UIFont.scaledNamedFont(.regular16)

standardAppearance = appearance
scrollEdgeAppearance = standardAppearance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class CoreHostingController<Content: View>: UIHostingController<CoreHosti
public var preferredStatusBarStyleOverride: ((UIViewController) -> UIStatusBarStyle)?

// MARK: - Public Properties
public var navigationBarStyle = UINavigationBar.Style.color(nil) // not applied until changed
public var navigationBarStyle = NavigationBarStyle.color(nil) // not applied until changed
public var defaultViewRoute: DefaultViewRouteParameters? {
didSet {
showDefaultDetailViewIfNeeded()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,7 @@ extension EnvironmentValues {
get { self[HorizontalPadding.self] }
set { self[HorizontalPadding.self] = newValue }
}

// Used for passing colors to NavigationBar components.
@Entry var navBarColors: NavigationBarColorConfiguration = .init(style: .modal)
}
10 changes: 0 additions & 10 deletions Core/Core/Common/CommonUI/ViewModifiers/NavBarItems.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,4 @@ extension View {
ToolbarItem(placement: .navigationBarTrailing, content: trailing)
}
}

/**
The built-in `.navigationTitle(Text)` modifier ignores all font and color modifiers on the text
so we use this `toolbar` based solution to have the title styled.
*/
public func navigationTitleStyled<T>(_ title: T) -> some View where T: View {
toolbar {
ToolbarItem(placement: .principal, content: { title })
}
}
}
Loading