Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion App/Composition/CompositionViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,14 @@ final class CompositionViewController: ViewController {

// Leave an escape hatch in case we were restored without an associated workspace. This can happen when a crash leaves old state information behind.
if navigationItem.leftBarButtonItem == nil {
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(CompositionViewController.didTapCancel))
let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(CompositionViewController.didTapCancel))
// Only set explicit tint color for iOS < 26
if #available(iOS 26.0, *) {
// Let iOS 26+ handle the color automatically
} else {
cancelButton.tintColor = theme["navigationBarTextColor"]
}
navigationItem.leftBarButtonItem = cancelButton
}
}

Expand Down
18 changes: 9 additions & 9 deletions App/Navigation/NavigationBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,22 @@ final class NavigationBar: UINavigationBar {
super.init(frame: frame)

// For whatever reason, translucent navbars with a barTintColor do not necessarily blur their backgrounds. An iPad 3, for example, blurs a bar without a barTintColor but is simply semitransparent with a barTintColor. The semitransparent, non-blur effect looks awful, so just turn it off.
isTranslucent = false

// iOS 26: Allow translucency for liquid glass effect
if #available(iOS 26.0, *) {
isTranslucent = true
} else {
isTranslucent = false
}

// Setting the barStyle to UIBarStyleBlack results in an appropriate status bar style.
barStyle = .black

backIndicatorImage = UIImage(named: "back")
backIndicatorTransitionMaskImage = UIImage(named: "back")
backIndicatorImage = UIImage(named: "back")?.withRenderingMode(.alwaysTemplate)
backIndicatorTransitionMaskImage = UIImage(named: "back")?.withRenderingMode(.alwaysTemplate)

titleTextAttributes = [.font: UIFont.preferredFontForTextStyle(.body, fontName: nil, sizeAdjustment: 0, weight: .regular)]

addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(didLongPress)))

if #available(iOS 15.0, *) {
// Fix odd grey navigation bar background when scrolled to top on iOS 15.
scrollEdgeAppearance = standardAppearance
}
}

required init?(coder: NSCoder) {
Expand Down
358 changes: 340 additions & 18 deletions App/Navigation/NavigationController.swift

Large diffs are not rendered by default.

20 changes: 17 additions & 3 deletions App/Posts/ReplyWorkspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,14 @@ final class ReplyWorkspace: NSObject {
}

let navigationItem = compositionViewController.navigationItem
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(ReplyWorkspace.didTapCancel(_:)))
let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(ReplyWorkspace.didTapCancel(_:)))
// Only set explicit tint color for iOS < 26
if #available(iOS 26.0, *) {
// Let iOS 26+ handle the color automatically
} else {
cancelButton.tintColor = compositionViewController.theme["navigationBarTextColor"]
}
navigationItem.leftBarButtonItem = cancelButton
navigationItem.rightBarButtonItem = rightButtonItem

$confirmBeforeReplying
Expand All @@ -139,7 +146,7 @@ final class ReplyWorkspace: NSObject {
fileprivate var textViewNotificationToken: AnyObject?

fileprivate lazy var rightButtonItem: UIBarButtonItem = { [unowned self] in
return UIBarButtonItem(title: self.draft.submitButtonTitle, style: .done, target: self, action: #selector(ReplyWorkspace.didTapPost(_:)))
return UIBarButtonItem(title: self.draft.submitButtonTitle, style: .plain, target: self, action: #selector(ReplyWorkspace.didTapPost(_:)))
}()

fileprivate func updateRightButtonItem() {
Expand Down Expand Up @@ -199,7 +206,14 @@ final class ReplyWorkspace: NSObject {
} else {
preview = PostPreviewViewController(thread: draft.thread, BBcode: draft.text ?? .init())
}
preview.navigationItem.rightBarButtonItem = UIBarButtonItem(title: draft.submitButtonTitle, style: .done, target: self, action: #selector(ReplyWorkspace.didTapPost(_:)))
let postButton = UIBarButtonItem(title: draft.submitButtonTitle, style: .plain, target: self, action: #selector(ReplyWorkspace.didTapPost(_:)))
// Only set explicit tint color for iOS < 26
if #available(iOS 26.0, *) {
// Let iOS 26+ handle the color automatically
} else {
postButton.tintColor = compositionViewController.theme["navigationBarTextColor"]
}
preview.navigationItem.rightBarButtonItem = postButton
(viewController as! UINavigationController).pushViewController(preview, animated: true)
}

Expand Down
163 changes: 161 additions & 2 deletions App/View Controllers/Messages/MessageViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ final class MessageViewController: ViewController {
renderView.delegate = self
return renderView
}()

private var _liquidGlassTitleView: UIView?

@available(iOS 26.0, *)
private var liquidGlassTitleView: LiquidGlassTitleView {
if _liquidGlassTitleView == nil {
let titleView = LiquidGlassTitleView()
titleView.title = privateMessage.subject
_liquidGlassTitleView = titleView
}
return _liquidGlassTitleView as! LiquidGlassTitleView
}

private lazy var replyButtonItem: UIBarButtonItem = {
return UIBarButtonItem(image: UIImage(named: "reply"), style: .plain, target: self, action: #selector(didTapReplyButtonItem))
Expand All @@ -55,7 +67,13 @@ final class MessageViewController: ViewController {
}

override var title: String? {
didSet { navigationItem.titleLabel.text = title }
didSet {
if #available(iOS 26.0, *) {
liquidGlassTitleView.title = title
} else {
navigationItem.titleLabel.text = title
}
}
}

private func renderMessage() {
Expand Down Expand Up @@ -194,10 +212,19 @@ final class MessageViewController: ViewController {

override func viewDidLoad() {
super.viewDidLoad()


extendedLayoutIncludesOpaqueBars = true

renderView.frame = CGRect(origin: .zero, size: view.bounds.size)
renderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
renderView.scrollView.contentInsetAdjustmentBehavior = .never
renderView.scrollView.delegate = self
view.insertSubview(renderView, at: 0)

if #available(iOS 26.0, *) {
configureNavigationBarForLiquidGlass()
configureLiquidGlassTitleView()
}

renderView.registerMessage(RenderView.BuiltInMessage.DidTapAuthorHeader.self)
renderView.registerMessage(RenderView.BuiltInMessage.DidFinishLoadingTweets.self)
Expand Down Expand Up @@ -286,8 +313,24 @@ final class MessageViewController: ViewController {
}

loadingView?.tintColor = theme["backgroundColor"]

if #available(iOS 26.0, *) {
if renderView.scrollView.contentOffset.y <= -renderView.scrollView.adjustedContentInset.top {
liquidGlassTitleView.textColor = theme["navigationBarTextColor"]
}
}
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

if #available(iOS 26.0, *) {
if let navController = navigationController as? NavigationController {
navController.updateNavigationBarTintForScrollProgress(NSNumber(value: 0.0))
}
}
}

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)

Expand All @@ -299,6 +342,94 @@ final class MessageViewController: ViewController {

userActivity = nil
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateScrollViewContentInsets()
}

override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
updateScrollViewContentInsets()
}

private func updateScrollViewContentInsets() {
renderView.scrollView.contentInset.top = view.safeAreaInsets.top
renderView.scrollView.contentInset.bottom = view.safeAreaInsets.bottom
renderView.scrollView.scrollIndicatorInsets = renderView.scrollView.contentInset
}

@available(iOS 26.0, *)
private func configureNavigationBarForLiquidGlass() {
guard let navigationBar = navigationController?.navigationBar else { return }
guard let navController = navigationController as? NavigationController else { return }

if let awfulNavigationBar = navigationBar as? NavigationBar {
awfulNavigationBar.bottomBorderColor = .clear
}

let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = theme["navigationBarTintColor"]
appearance.shadowColor = nil
appearance.shadowImage = nil

let textColor: UIColor = theme["navigationBarTextColor"]!
appearance.titleTextAttributes = [
NSAttributedString.Key.foregroundColor: textColor,
NSAttributedString.Key.font: UIFont.preferredFontForTextStyle(.body, fontName: nil, sizeAdjustment: 0, weight: .semibold)
]

let buttonFont = UIFont.preferredFontForTextStyle(.body, fontName: nil, sizeAdjustment: 0, weight: .regular)
let buttonAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: textColor,
.font: buttonFont
]
appearance.buttonAppearance.normal.titleTextAttributes = buttonAttributes
appearance.buttonAppearance.highlighted.titleTextAttributes = buttonAttributes
appearance.doneButtonAppearance.normal.titleTextAttributes = buttonAttributes
appearance.doneButtonAppearance.highlighted.titleTextAttributes = buttonAttributes
appearance.backButtonAppearance.normal.titleTextAttributes = buttonAttributes
appearance.backButtonAppearance.highlighted.titleTextAttributes = buttonAttributes

if let backImage = UIImage(named: "back")?.withRenderingMode(.alwaysTemplate) {
appearance.setBackIndicatorImage(backImage, transitionMaskImage: backImage)
}

navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = appearance
navigationBar.compactAppearance = appearance
navigationBar.compactScrollEdgeAppearance = appearance

navigationBar.tintColor = textColor

navController.updateNavigationBarTintForScrollProgress(NSNumber(value: 0.0))

navigationBar.setNeedsLayout()
}

@available(iOS 26.0, *)
private func configureLiquidGlassTitleView() {
liquidGlassTitleView.textColor = theme["navigationBarTextColor"]

switch UIDevice.current.userInterfaceIdiom {
case .pad:
liquidGlassTitleView.font = UIFont.preferredFontForTextStyle(.callout, fontName: nil, sizeAdjustment: 0, weight: .semibold)
default:
liquidGlassTitleView.font = UIFont.preferredFontForTextStyle(.callout, fontName: nil, sizeAdjustment: 0, weight: .semibold)
}

navigationItem.titleView = liquidGlassTitleView
}

@available(iOS 26.0, *)
private func updateTitleViewTextColorForScrollProgress(_ progress: CGFloat) {
if progress < 0.01 {
liquidGlassTitleView.textColor = theme["navigationBarTextColor"]
} else if progress > 0.99 {
liquidGlassTitleView.textColor = nil
}
}

private enum CodingKey {
static let composeViewController = "AwfulComposeViewController"
Expand Down Expand Up @@ -397,6 +528,34 @@ extension MessageViewController: UIGestureRecognizerDelegate {
}
}

extension MessageViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if #available(iOS 26.0, *) {
let topInset = scrollView.adjustedContentInset.top
let currentOffset = scrollView.contentOffset.y
let topPosition = -topInset

let transitionDistance: CGFloat = 30.0

let progress: CGFloat
if currentOffset <= topPosition {
progress = 0.0
} else if currentOffset >= topPosition + transitionDistance {
progress = 1.0
} else {
let distanceFromTop = currentOffset - topPosition
progress = distanceFromTop / transitionDistance
}

if let navController = navigationController as? NavigationController {
navController.updateNavigationBarTintForScrollProgress(NSNumber(value: Float(progress)))
}

updateTitleViewTextColorForScrollProgress(progress)
}
}
}

extension MessageViewController: UIViewControllerRestoration {
static func viewController(withRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
guard let messageKey = coder.decodeObject(of: PrivateMessageKey.self, forKey: CodingKey.message) else {
Expand Down
58 changes: 58 additions & 0 deletions App/View Controllers/Posts/GradientView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// GradientView.swift
//
// Copyright 2025 Awful Contributors. CC BY-NC-SA 3.0 US

import AwfulTheming
import UIKit

/// A UIView subclass that uses CAGradientLayer as its backing layer.
final class GradientView: UIView {

override class var layerClass: AnyClass {
CAGradientLayer.self
}

private var gradientLayer: CAGradientLayer {
layer as! CAGradientLayer
}

override init(frame: CGRect) {
super.init(frame: frame)
configureGradient()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
configureGradient()
}

private func configureGradient() {
let isDarkMode = Theme.defaultTheme()[string: "mode"] == "dark"

if isDarkMode {
gradientLayer.colors = [
UIColor.black.cgColor,
UIColor.black.withAlphaComponent(0.8).cgColor,
UIColor.black.withAlphaComponent(0.4).cgColor,
UIColor.clear.cgColor
]
gradientLayer.locations = [0.0, 0.3, 0.7, 1.0]
} else {
gradientLayer.colors = [
UIColor.white.withAlphaComponent(0.8).cgColor,
UIColor.white.withAlphaComponent(0.6).cgColor,
UIColor.white.withAlphaComponent(0.2).cgColor,
UIColor.white.withAlphaComponent(0.02).cgColor,
UIColor.clear.cgColor
]
gradientLayer.locations = [0.0, 0.4, 0.7, 0.9, 1.0]
}

gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1.0)
}

func themeDidChange() {
configureGradient()
}
}
22 changes: 22 additions & 0 deletions App/View Controllers/Posts/PostsPageRefreshSpinnerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ final class PostsPageRefreshSpinnerView: UIView, PostsPageRefreshControlContent
arrows.topAnchor.constraint(equalTo: topAnchor).isActive = true
arrows.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
arrows.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
alpha = 0.0
}

required init?(coder: NSCoder) {
Expand Down Expand Up @@ -90,6 +91,27 @@ final class PostsPageRefreshSpinnerView: UIView, PostsPageRefreshControlContent
var state: PostsPageView.RefreshControlState = .ready {
didSet {
transition(from: oldValue, to: state)

switch state {
case .ready, .disabled:
UIView.animate(withDuration: 0.2) {
self.alpha = 0.0
}

case .armed(let triggeredFraction):
let targetAlpha = min(1.0, triggeredFraction * 2)
UIView.animate(withDuration: 0.1) {
self.alpha = targetAlpha
}

case .triggered, .refreshing:
UIView.animate(withDuration: 0.2) {
self.alpha = 1.0
}

case .awaitingScrollEnd:
break
}
}
}
}
Expand Down
Loading
Loading