Update message context menu for iOS 26

This commit is contained in:
Elaine 2025-10-21 16:49:06 -04:00 committed by GitHub
parent 3de0eb06ba
commit e4d61c17b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 123 additions and 45 deletions

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -10,6 +10,7 @@
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -150,12 +150,10 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
private class ContextMenuActionRow: UIView {
let attributes: ContextMenuAction.Attributes
let hostEffect: UIBlurEffect
let forceDarkTheme: Bool
let visualEffectView: UIVisualEffectView
let titleLabel: UILabel
let iconView: UIImageView
let separatorView: UIView
var separatorView: UIView?
var highlightedView: UIView?
var isHighlighted: Bool {
didSet {
@ -163,7 +161,16 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
if isHighlighted {
if highlightedView == nil {
let view = UIView()
view.frame = bounds
if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
#if compiler(>=6.2)
view.frame = bounds.insetBy(dx: 12, dy: 4)
view.cornerConfiguration = .capsule()
#endif
} else {
view.frame = bounds
}
view.backgroundColor = forceDarkTheme || Theme.isDarkThemeEnabled
? UIColor(rgbHex: 0x787880).withAlphaComponent(0.32)
: UIColor(rgbHex: 0x787880).withAlphaComponent(0.16)
@ -182,16 +189,25 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
}
var maxWidth: CGFloat = 250
let margin: CGFloat = 16
let margin: CGFloat = if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
24
} else {
16
}
let iconSpacing: CGFloat = 12
let verticalPadding: CGFloat = 23
let iconSize: CGFloat = 20
let iconSize: CGFloat = if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
24
} else {
20
}
let titleMaxLines = 2
public init(
title: String,
icon: UIImage?,
attributes: ContextMenuAction.Attributes,
hostBlurEffect: UIBlurEffect,
hostBlurEffect: UIBlurEffect?,
forceDarkTheme: Bool
) {
titleLabel = UILabel(frame: CGRect.zero)
@ -200,7 +216,6 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
titleLabel.numberOfLines = titleMaxLines
self.attributes = attributes
hostEffect = hostBlurEffect
self.forceDarkTheme = forceDarkTheme
/// when made a child of a UIVisualEffectView, UILabel text color is overridden, but a vibrancy effect is added.
@ -222,26 +237,34 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
iconView.contentMode = .scaleAspectFit
iconView.tintColor = titleLabel.textColor
separatorView = UIView()
separatorView.backgroundColor = forceDarkTheme || Theme.isDarkThemeEnabled
? UIColor(rgbHex: 0x545458).withAlphaComponent(0.6)
: UIColor(rgbHex: 0x3c3c43).withAlphaComponent(0.3)
separatorView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
isHighlighted = false
visualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: hostBlurEffect, style: .label))
super.init(frame: .zero)
addSubview(visualEffectView)
if makeLabelSubviewOfVisualEffectsView {
visualEffectView.contentView.addSubview(titleLabel)
if let hostBlurEffect {
let visualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: hostBlurEffect, style: .label))
addSubview(visualEffectView)
if makeLabelSubviewOfVisualEffectsView {
visualEffectView.contentView.addSubview(titleLabel)
} else {
addSubview(titleLabel)
}
let separatorView = UIView()
separatorView.backgroundColor = forceDarkTheme || Theme.isDarkThemeEnabled
? UIColor(rgbHex: 0x545458).withAlphaComponent(0.6)
: UIColor(rgbHex: 0x3c3c43).withAlphaComponent(0.3)
separatorView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.separatorView = separatorView
visualEffectView.contentView.addSubview(iconView)
visualEffectView.contentView.addSubview(separatorView)
visualEffectView.autoPinEdgesToSuperviewEdges()
} else {
addSubview(titleLabel)
addSubview(iconView)
}
visualEffectView.contentView.addSubview(iconView)
visualEffectView.contentView.addSubview(separatorView)
visualEffectView.autoPinEdgesToSuperviewEdges()
isAccessibilityElement = true
accessibilityLabel = titleLabel.text
@ -263,7 +286,7 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
titleFrame.y = ceil((bounds.height - titleFrame.height) / 2)
iconViewFrame.y = max(0, (bounds.height - iconViewFrame.height) / 2)
let titleWidth = bounds.width - iconViewFrame.width - 3 * margin
let titleWidth = bounds.width - iconViewFrame.width - (2 * margin) - iconSpacing
if titleWidth < titleFrame.width {
// Give it more height for a second line.
let originalHeight = titleLabel.textRect(forBounds: CGRect.infinite, limitedToNumberOfLines: 1).height
@ -280,27 +303,35 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
}
titleFrame.width = titleWidth
if !isRTL {
let iconIsToTheRightOfText = if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
isRTL
} else {
!isRTL
}
if iconIsToTheRightOfText {
titleFrame.x = margin
iconViewFrame.x = titleFrame.maxX + margin
iconViewFrame.x = titleFrame.maxX + iconSpacing
} else {
iconViewFrame.x = margin
titleFrame.x = iconViewFrame.maxX + margin
titleFrame.x = iconViewFrame.maxX + iconSpacing
}
titleLabel.frame = titleFrame
iconView.frame = iconViewFrame
var separatorFrame = bounds
separatorFrame.height = 1.0 / UIScreen.main.scale
separatorFrame.y = bounds.maxY - separatorFrame.height
separatorView.frame = separatorFrame
if let separatorView {
var separatorFrame = bounds
separatorFrame.height = 1.0 / UIScreen.main.scale
separatorFrame.y = bounds.maxY - separatorFrame.height
separatorView.frame = separatorFrame
}
}
public override func sizeThatFits(
_ size: CGSize
) -> CGSize {
let height = ceil(titleLabel.sizeThatFits(CGSize(width: maxWidth - 3 * margin - iconSize, height: 0)).height) + verticalPadding
let height = ceil(titleLabel.sizeThatFits(CGSize(width: maxWidth - (2 * margin) - iconSpacing - iconSize, height: 0)).height) + verticalPadding
return CGSize(width: maxWidth, height: height)
}
}
@ -326,7 +357,17 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
}
}
let cornerRadius: CGFloat = 12
let cornerRadius: CGFloat = if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
33
} else {
12
}
let vMargin: CGFloat = if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
10
} else {
0
}
public init(
menu: ContextMenu,
@ -336,15 +377,36 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
self.forceDarkTheme = forceDarkTheme
scrollView = UIScrollView(frame: CGRect.zero)
let effect: UIBlurEffect = .init(style: .systemThinMaterial)
backdropView = UIVisualEffectView(effect: effect)
backdropView.backgroundColor = Theme.isDarkThemeEnabled || forceDarkTheme
? .ows_blackAlpha80
: .ows_whiteAlpha40
scrollView.verticalScrollIndicatorInsets = .init(hMargin: 0, vMargin: cornerRadius)
let blurEffect: UIBlurEffect?
if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
#if compiler(>=6.2)
let effect = UIGlassEffect(style: .clear)
effect.isInteractive = true
backdropView = UIVisualEffectView(effect: effect)
#else
// will never execute because of the FeatureFlags.iOS26SDKIsAvailable check above, but needed for the Xcode 16 compiler to not get mad. Remove when CI is all Xcode 26
backdropView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThinMaterial))
#endif
blurEffect = nil
} else {
blurEffect = UIBlurEffect(style: .systemThinMaterial)
backdropView = UIVisualEffectView(effect: blurEffect)
backdropView.backgroundColor = Theme.isDarkThemeEnabled || forceDarkTheme
? .ows_blackAlpha80
: .ows_whiteAlpha40
}
var actionViews: [ContextMenuActionRow] = []
for action in menu.children {
let actionView = ContextMenuActionRow(title: action.title, icon: action.image, attributes: action.attributes, hostBlurEffect: effect, forceDarkTheme: forceDarkTheme)
let actionView = ContextMenuActionRow(
title: action.title,
icon: action.image,
attributes: action.attributes,
hostBlurEffect: blurEffect,
forceDarkTheme: forceDarkTheme
)
actionViews.append(actionView)
}
@ -365,11 +427,14 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
addGestureRecognizer(highlightHoverGestureRecognizer)
self.highlightHoverGestureRecognizer = highlightHoverGestureRecognizer
layer.cornerRadius = cornerRadius
layer.shadowRadius = 64
layer.shadowOffset = CGSize(width: 0, height: 32)
layer.shadowColor = UIColor.ows_black.cgColor
layer.shadowOpacity = 0.2
if #available(iOS 26, *), FeatureFlags.iOS26SDKIsAvailable {
} else {
layer.cornerRadius = cornerRadius
layer.shadowRadius = 64
layer.shadowOffset = CGSize(width: 0, height: 32)
layer.shadowColor = UIColor.ows_black.cgColor
layer.shadowOpacity = 0.2
}
backdropView.layer.cornerRadius = cornerRadius
backdropView.layer.masksToBounds = true
@ -382,7 +447,7 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
scrollView.delegate = self
actionViews.last?.separatorView.isHidden = true
actionViews.last?.separatorView?.isHidden = true
}
required init?(coder: NSCoder) {
@ -396,7 +461,7 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
backdropView.frame = bounds
scrollView.frame = bounds
var yOffset: CGFloat = 0
var yOffset: CGFloat = vMargin
var maxY: CGFloat = 0
var width: CGFloat = 0.0
for actionView in actionViews {
@ -406,6 +471,7 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
yOffset += size.height
maxY = max(maxY, actionView.frame.maxY)
}
maxY += vMargin
scrollView.contentSize = CGSize(width: width, height: maxY)
}
@ -414,7 +480,7 @@ private class ContextMenuActionsView: UIView, UIGestureRecognizerDelegate, UIScr
_ size: CGSize
) -> CGSize {
// every entry may have a different height
var height = 0.0
var height = vMargin * 2
var width = 0.0
for actionView in actionViews {
let size = actionView.sizeThatFits(size)