Add scaledFrame/scaledPadding APIs (#2079)

* Add scaled frame and padding modifiers

* Refactor AppLockScreen

* Refactor FormattingToolbar

* Refactor RoomAttachmentPicker

* Refactor ComposerToolbar

* Refactor VoiceMessageRecordingButton

* More refactors

* Refine ScaledPaddingModifier

* Cleanup

* Fix preview tests

* Fix preview test

* Use CompoundIcon for custom image

* Add .scaledFrame(size:) overload

* Set fallback style to .body
This commit is contained in:
Alfonso Grillo
2023-11-14 12:20:34 +01:00
committed by GitHub
parent 4257c20bc9
commit 752ad66397
43 changed files with 172 additions and 121 deletions
@@ -125,14 +125,12 @@ struct FormButtonStyle: PrimitiveButtonStyle {
struct FormActionButtonStyle: ButtonStyle {
let title: String
@ScaledMetric private var menuIconSize = 54.0
func makeBody(configuration: Configuration) -> some View {
VStack {
configuration.label
.buttonStyle(.plain)
.foregroundColor(.compound.textPrimary)
.frame(width: menuIconSize, height: menuIconSize)
.scaledFrame(size: 54)
.background {
RoundedRectangle(cornerRadius: 16)
.fill(configuration.isPressed ? Color.compound.bgSubtlePrimary : .compound.bgCanvasDefaultLevel1)
@@ -17,8 +17,6 @@
import SwiftUI
struct FormRowLabelStyle: LabelStyle {
@ScaledMetric private var menuIconSize = 30.0
var alignment = VerticalAlignment.firstTextBaseline
var role: ButtonRole?
@@ -51,7 +49,7 @@ struct FormRowLabelStyle: LabelStyle {
configuration.icon
.foregroundColor(iconForegroundColor)
.padding(4)
.frame(width: menuIconSize, height: menuIconSize)
.scaledFrame(size: 30)
.background(iconBackgroundColor)
.clipShape(RoundedRectangle(cornerRadius: 8))
configuration.title
@@ -0,0 +1,43 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
extension View {
func scaledFrame(size: CGFloat, alignment: Alignment = .center, relativeTo textStyle: Font.TextStyle = .body) -> some View {
scaledFrame(width: size, height: size, alignment: alignment, relativeTo: textStyle)
}
func scaledFrame(width: CGFloat, height: CGFloat, alignment: Alignment = .center, relativeTo textStyle: Font.TextStyle = .body) -> some View {
modifier(ScaledFrameModifier(width: width, height: height, alignment: alignment, relativeTo: textStyle))
}
}
private struct ScaledFrameModifier: ViewModifier {
@ScaledMetric var width: CGFloat
@ScaledMetric var height: CGFloat
let alignment: Alignment
init(width: CGFloat, height: CGFloat, alignment: Alignment, relativeTo textStyle: Font.TextStyle) {
_width = ScaledMetric(wrappedValue: width, relativeTo: textStyle)
_height = ScaledMetric(wrappedValue: height, relativeTo: textStyle)
self.alignment = alignment
}
func body(content: Content) -> some View {
content.frame(width: width, height: height, alignment: alignment)
}
}
@@ -0,0 +1,41 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
extension View {
func scaledPadding(_ length: CGFloat, relativeTo textStyle: Font.TextStyle = .body) -> some View {
scaledPadding(.all, length, relativeTo: textStyle)
}
func scaledPadding(_ edges: Edge.Set, _ length: CGFloat, relativeTo textStyle: Font.TextStyle = .body) -> some View {
modifier(ScaledPaddingModifier(edges: edges, length: length, textStyle: textStyle))
}
}
private struct ScaledPaddingModifier: ViewModifier {
let edges: Edge.Set
@ScaledMetric var length: CGFloat
init(edges: Edge.Set, length: CGFloat, textStyle: Font.TextStyle) {
self.edges = edges
_length = ScaledMetric(wrappedValue: length, relativeTo: textStyle)
}
func body(content: Content) -> some View {
content.padding(edges, length)
}
}
@@ -18,8 +18,6 @@ import SwiftUI
struct VoiceMessageButton: View {
@ScaledMetric private var buttonSize: CGFloat
@ScaledMetric private var imageWidth: CGFloat = 12
@ScaledMetric private var imageHeight: CGFloat = 14
enum State {
case loading
@@ -71,7 +69,7 @@ struct VoiceMessageButton: View {
Image(asset: imageAsset)
.resizable()
.scaledToFit()
.frame(width: imageWidth, height: imageHeight)
.scaledFrame(width: 12, height: 14)
.offset(x: offset)
}
}
@@ -20,8 +20,6 @@ import SwiftUI
struct AppLockScreen: View {
@ObservedObject var context: AppLockScreenViewModel.Context
/// The size of each dot within the PIN input field.
@ScaledMetric private var pinDotSize = 14
/// Used to animate the PIN input field on failure.
@State private var pinInputFieldOffset = 0.0
@@ -88,18 +86,20 @@ struct AppLockScreen: View {
/// The row of dots showing how many digits have been entered.
var pinInputField: some View {
HStack(spacing: 24) {
/// The size of each dot within the PIN input field.
let pinDotSize: CGFloat = 14
Circle()
.fill(context.viewState.numberOfDigitsEntered > 0 ? .compound.iconPrimary : .compound.bgSubtlePrimary)
.frame(width: pinDotSize, height: pinDotSize)
.scaledFrame(size: pinDotSize)
Circle()
.fill(context.viewState.numberOfDigitsEntered > 1 ? .compound.iconPrimary : .compound.bgSubtlePrimary)
.frame(width: pinDotSize, height: pinDotSize)
.scaledFrame(size: pinDotSize)
Circle()
.fill(context.viewState.numberOfDigitsEntered > 2 ? .compound.iconPrimary : .compound.bgSubtlePrimary)
.frame(width: pinDotSize, height: pinDotSize)
.scaledFrame(size: pinDotSize)
Circle()
.fill(context.viewState.numberOfDigitsEntered > 3 ? .compound.iconPrimary : .compound.bgSubtlePrimary)
.frame(width: pinDotSize, height: pinDotSize)
.scaledFrame(size: pinDotSize)
}
}
@@ -24,13 +24,6 @@ struct ComposerToolbar: View {
let keyCommandHandler: KeyCommandHandler
@FocusState private var composerFocused: Bool
@ScaledMetric(relativeTo: .title) private var sendButtonIconSize = 16
@ScaledMetric(relativeTo: .title) private var sendButtonIconPadding = 10
@ScaledMetric(relativeTo: .title) private var sendButtonIconOffsetX = 1
@ScaledMetric(relativeTo: .title) private var spinnerSize = 44
@ScaledMetric(relativeTo: .title) private var closeRTEButtonSize = 30
@ScaledMetric(relativeTo: .title) private var deleteRecordingButtonSize = 30
@State private var frame: CGRect = .zero
@Environment(\.verticalSizeClass) private var verticalSizeClass
@@ -75,7 +68,7 @@ struct ComposerToolbar: View {
if !context.composerActionsEnabled {
if context.viewState.isUploading {
ProgressView()
.frame(width: spinnerSize, height: spinnerSize)
.scaledFrame(size: 44, relativeTo: .title)
.padding(.leading, 3)
} else if context.viewState.showSendButton {
sendButton
@@ -139,8 +132,8 @@ struct ComposerToolbar: View {
Image(Asset.Images.closeRte.name)
.resizable()
.scaledToFit()
.frame(width: closeRTEButtonSize, height: closeRTEButtonSize)
.padding(7)
.scaledFrame(size: 30, relativeTo: .title)
.scaledPadding(7, relativeTo: .title)
}
.accessibilityLabel(L10n.actionClose)
.accessibilityIdentifier(A11yIdentifiers.roomScreen.composerToolbar.closeFormattingOptions)
@@ -158,7 +151,7 @@ struct ComposerToolbar: View {
Circle()
.foregroundColor(context.viewState.sendButtonDisabled ? .clear : .compound.iconAccentTertiary)
}
.padding(4)
.scaledPadding(4, relativeTo: .title)
}
.disabled(context.viewState.sendButtonDisabled)
.animation(.linear(duration: 0.1).disabledDuringTests(), value: context.viewState.sendButtonDisabled)
@@ -217,11 +210,13 @@ struct ComposerToolbar: View {
.opacity(context.viewState.composerMode.isEdit ? 1 : 0)
.accessibilityLabel(L10n.actionConfirm)
.accessibilityHidden(!context.viewState.composerMode.isEdit)
let sendImageOffset: CGFloat = 1
Image(asset: Asset.Images.sendMessage)
.resizable()
.offset(x: sendButtonIconOffsetX)
.frame(width: sendButtonIconSize, height: sendButtonIconSize)
.padding(sendButtonIconPadding)
.scaledToFit()
.scaledFrame(width: 16 + sendImageOffset, height: 16, alignment: .trailing, relativeTo: .title)
.scaledPadding(10, relativeTo: .title)
.opacity(context.viewState.composerMode.isEdit ? 0 : 1)
.accessibilityLabel(L10n.actionSend)
.accessibilityHidden(context.viewState.composerMode.isEdit)
@@ -270,8 +265,8 @@ struct ComposerToolbar: View {
} label: {
CompoundIcon(\.delete)
.scaledToFit()
.frame(width: deleteRecordingButtonSize, height: deleteRecordingButtonSize)
.padding(7)
.scaledFrame(size: 30, relativeTo: .title)
.scaledPadding(7, relativeTo: .title)
}
.buttonStyle(.compound(.plain))
.accessibilityLabel(L10n.a11yDelete)
@@ -22,8 +22,6 @@ struct FormattingToolbar: View {
/// The action when an item is selected
var formatAction: (FormatType) -> Void
@ScaledMetric private var toolbarButtonIconSize = 20
var body: some View {
ScrollView(.horizontal) {
HStack(spacing: 4) {
@@ -34,7 +32,7 @@ struct FormattingToolbar: View {
item.icon
.resizable()
.scaledToFit()
.frame(width: toolbarButtonIconSize, height: toolbarButtonIconSize)
.scaledFrame(size: 20)
.foregroundColor(item.foregroundColor)
.padding(8)
.background(item.backgroundColor)
@@ -21,7 +21,6 @@ import WysiwygComposer
struct RoomAttachmentPicker: View {
@ObservedObject var context: ComposerToolbarViewModel.Context
@Environment(\.isPresented) var isPresented
@ScaledMetric(relativeTo: .title) private var attachmentButtonSize: CGFloat = 30
@State private var sheetContentHeight = CGFloat(0)
@@ -32,9 +31,9 @@ struct RoomAttachmentPicker: View {
Image(Asset.Images.composerAttachment.name)
.resizable()
.scaledToFit()
.frame(width: attachmentButtonSize, height: attachmentButtonSize)
.scaledFrame(size: 30, relativeTo: .title)
.foregroundColor(.compound.textActionPrimary)
.padding(7)
.scaledPadding(7, relativeTo: .title)
}
.accessibilityLabel(L10n.actionAddToTimeline)
.accessibilityIdentifier(A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions)
@@ -28,11 +28,6 @@ struct VoiceMessageRecordingButton: View {
var stopRecording: (() -> Void)?
private let impactFeedbackGenerator = UIImpactFeedbackGenerator()
@ScaledMetric(relativeTo: .title) private var idleImageSize = 16
@ScaledMetric(relativeTo: .title) private var idleImagePadding = 10
@ScaledMetric(relativeTo: .title) private var recordingImageSize = 24
@ScaledMetric(relativeTo: .title) private var recordingImagePadding = 6
var body: some View {
Button {
@@ -48,20 +43,16 @@ struct VoiceMessageRecordingButton: View {
case .idle:
CompoundIcon(\.micOnOutline, size: .medium, relativeTo: .title)
.foregroundColor(.compound.iconSecondary)
.frame(width: idleImageSize, height: idleImageSize)
.padding(idleImagePadding)
.padding(4)
.scaledPadding(10, relativeTo: .title)
case .recording:
Asset.Images.stopRecording.swiftUIImage
.resizable()
CompoundIcon(customImage: Asset.Images.stopRecording.swiftUIImage, size: .medium, relativeTo: .title)
.foregroundColor(.compound.iconOnSolidPrimary)
.frame(width: recordingImageSize, height: recordingImageSize)
.padding(recordingImagePadding)
.scaledPadding(6, relativeTo: .title)
.background(
Circle()
.foregroundColor(.compound.bgActionPrimaryRest)
)
.padding(4)
.scaledPadding(4, relativeTo: .title)
}
}
.buttonStyle(VoiceMessageRecordingButtonStyle())
@@ -45,7 +45,6 @@ struct CreateRoomScreen: View {
.alert(item: $context.alertInfo)
}
@ScaledMetric private var roomIconSize: CGFloat = 70
private var roomSection: some View {
Section {
HStack(alignment: .center, spacing: 16) {
@@ -61,7 +60,7 @@ struct CreateRoomScreen: View {
} placeholder: {
ProgressView()
}
.frame(width: roomIconSize, height: roomIconSize)
.scaledFrame(size: 70)
.clipShape(Circle())
} else {
cameraImage
@@ -106,7 +105,7 @@ struct CreateRoomScreen: View {
Image(systemName: "camera")
.font(.system(size: 28, weight: .semibold))
.foregroundColor(.compound.iconSecondary)
.frame(width: roomIconSize, height: roomIconSize)
.scaledFrame(size: 70)
.background(Color.compound.bgSubtlePrimary)
.clipShape(Circle())
}
@@ -17,8 +17,6 @@
import SwiftUI
struct HomeScreenInvitesButton: View {
@ScaledMetric private var badgeSize = 12.0
let title: String
let hasBadge: Bool
let action: () -> Void
@@ -44,7 +42,7 @@ struct HomeScreenInvitesButton: View {
private var badge: some View {
Circle()
.frame(width: badgeSize, height: badgeSize)
.scaledFrame(size: 12)
.foregroundColor(.compound.iconAccentTertiary)
}
}
@@ -21,8 +21,6 @@ struct InviteUsersScreenSelectedItem: View {
let imageProvider: ImageProviderProtocol?
let dismissAction: () -> Void
@ScaledMetric private var buttonSize: CGFloat = 20
var body: some View {
VStack(spacing: 0) {
avatar
@@ -46,7 +44,7 @@ struct InviteUsersScreenSelectedItem: View {
Button(action: dismissAction) {
Image(systemName: "xmark.circle.fill")
.resizable()
.frame(width: buttonSize, height: buttonSize)
.scaledFrame(size: 20)
.symbolRenderingMode(.palette)
.foregroundStyle(Color.compound.iconOnSolidPrimary, Color.compound.iconPrimary)
}
@@ -23,8 +23,6 @@ struct InvitesScreenCell: View {
let acceptAction: () -> Void
let declineAction: () -> Void
@ScaledMetric private var badgeSize = 12.0
var body: some View {
HStack(alignment: .top, spacing: 16) {
LoadableAvatarImage(url: invite.roomDetails.avatarURL,
@@ -150,7 +148,7 @@ struct InvitesScreenCell: View {
private var badge: some View {
Circle()
.frame(width: badgeSize, height: badgeSize)
.scaledFrame(size: 12)
.foregroundColor(.compound.iconAccentTertiary)
}
}
@@ -104,7 +104,6 @@ struct StaticLocationScreen: View {
context.viewState.isLocationPickerMode ? .horizontal : [.horizontal, .bottom]
}
@ScaledMetric private var shareMarkerSize: CGFloat = 28
private var selectLocationButton: some View {
Button {
context.send(viewAction: .selectLocation)
@@ -113,7 +112,7 @@ struct StaticLocationScreen: View {
Image(asset: Asset.Images.locationMarker)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: shareMarkerSize, height: shareMarkerSize)
.scaledFrame(size: 28)
Text(context.viewState.isSharingUserLocation ? L10n.screenShareMyLocationAction : L10n.screenShareThisLocationAction)
}
}
@@ -17,8 +17,6 @@
import SwiftUI
struct RoomMembersListScreenMemberCell: View {
@ScaledMetric private var avatarSize = AvatarSize.user(on: .roomDetails).value
let member: RoomMemberDetails
let context: RoomMembersListScreenViewModel.Context
@@ -25,8 +25,6 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
let timelineItem: EventBasedTimelineItemProtocol
@ViewBuilder let content: () -> Content
@ScaledMetric private var senderNameVerticalPadding = 3
@State private var showItemActionMenu = false
@@ -85,7 +83,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
.font(.compound.bodySMSemibold)
.foregroundColor(.compound.avatarColor(for: timelineItem.sender.id).foreground)
.lineLimit(1)
.padding(.vertical, senderNameVerticalPadding)
.scaledPadding(.vertical, 3)
}
// sender info are read inside the `TimelineAccessibilityModifier`
.accessibilityHidden(true)
@@ -191,13 +191,11 @@ struct TimelineReactionButton: View {
}
struct TimelineReactionAddMoreButtonLabel: View {
@ScaledMetric private var addMoreButtonIconSize = 16
var body: some View {
TimelineReactionButtonLabel {
Image(asset: Asset.Images.addReaction)
.resizable()
.frame(width: addMoreButtonIconSize, height: addMoreButtonIconSize)
.scaledFrame(size: 16)
.padding(.vertical, 8)
.padding(.horizontal, 12)
.foregroundColor(.compound.iconSecondary)
@@ -20,8 +20,6 @@ struct PollRoomTimelineView: View {
let timelineItem: PollRoomTimelineItem
@Environment(\.timelineStyle) var timelineStyle
@EnvironmentObject private var context: RoomScreenViewModel.Context
@ScaledMetric private var summaryPadding = 32
@ScaledMetric private var iconSize = 22
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
@@ -53,7 +51,7 @@ struct PollRoomTimelineView: View {
Image(asset.name)
.resizable()
.frame(width: iconSize, height: iconSize)
.scaledFrame(size: 22)
.accessibilityHidden(true)
Text(poll.question)
@@ -83,7 +81,7 @@ struct PollRoomTimelineView: View {
if let summaryText = poll.summaryText {
Text(summaryText)
.font(.compound.bodySM)
.padding(.leading, showVotes ? 0 : summaryPadding)
.scaledPadding(.leading, showVotes ? 0 : 32)
.foregroundColor(.compound.textSecondary)
.frame(maxWidth: .infinity, alignment: showVotes ? .trailing : .leading)
}
@@ -136,7 +136,6 @@ extension RoomTimelineItemProtocol {
public struct TimelineItemMenu: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.dismiss) private var dismiss
@ScaledMetric private var addMoreButtonIconSize = 24
let item: EventBasedTimelineItemProtocol
let actions: TimelineItemMenuActions
@@ -228,7 +227,7 @@ public struct TimelineItemMenu: View {
} label: {
Image(asset: Asset.Images.addReaction)
.resizable()
.frame(width: addMoreButtonIconSize, height: addMoreButtonIconSize)
.scaledFrame(size: 24)
.frame(maxHeight: .infinity, alignment: .center)
.foregroundColor(.compound.iconSecondary)
.padding(10)
@@ -19,8 +19,7 @@ import SwiftUI
struct TimelineSenderAvatarView: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
@ScaledMetric private var avatarSize = AvatarSize.user(on: .timeline).value
let timelineItem: EventBasedTimelineItemProtocol
var body: some View {