Привет, товарищи iOS-разработчики! Я очень рад поделиться с вами этой записью в блоге! Долгое время мне не терпелось рассказать вам все о моей новообретенной любви: contextMenu. По общему признанию, я не использовал contextMenus в прошлом и не реализовал весь его потенциал. Но изучив красивое приложение Apple Music, чтобы воспроизвести его для своего портфолио, я наконец понял магию contextMenus.
Если вы хотите сделать свое приложение более отзывчивым или просто хотите предоставить своим пользователям доступ к определенным функциям с помощью различных методов, таких как длительное нажатие или скрытая кнопка, то вам обязательно нужно прочитать этот пост! Я покажу вам не только, как и когда использовать contextMenus, но и как настроить предварительный просмотр для них в iOS 15. Поверьте, вы не захотите это пропустить!
Контекстное меню обеспечивает доступ к функциям, непосредственно связанным с элементом на экране, не загромождая интерфейс. В этом случае элементом на экране является Song Cell. ContextMenu открывается при длительном нажатии, определяемом системой.
В начале я хотел бы предупредить вас о неправильном использовании contextMenus. В соответствии с рекомендациями Apple по пользовательскому интерфейсу:
Всегда делайте пункты контекстного меню доступными и в основном интерфейсе. Например, в приложении «Почта» в iOS и iPadOS элементы контекстного меню, доступные для сообщения в папке «Входящие», также доступны на панели инструментов представления сообщения. В macOS в меню строки меню приложения перечислены все команды приложения, в том числе в различных контекстных меню.
Предварительный просмотр contextMenu — это предварительный просмотр элемента или представления, которое будет представлено при основном действии элемента. В случае с Apple Music Song Cell показывает: название альбома, год выпуска и тип песни. Все эти данные не отображаются в нормальном состоянии Song Cell. Это означает, что предварительный просмотр также можно использовать для преобразования существующей ячейки.
Теперь давайте запачкаем руки!
Во-первых, давайте создадим новый проект с пустым представлением SwiftUI. Внутри этого представления вставьте код ниже:
@State private var size = CGSize(width: 50, height: 50) var body: some View { RoundedRectangle(cornerRadius: 10) .frame(width: 100, height: 100) .foregroundColor(.green) .overlay { Text("Context") .foregroundColor(.white) .font(.title2) .cornerRadius(10) .shadow(radius: 3.5) .frame(width: 100, height: 100) } .contextMenu { //Our context Menu actions selection } }
Вот что вы должны увидеть в предварительном просмотре контента:
Поскольку в нашем модификаторе contextMenu нет элементов, даже если мы долго нажимаем на наш RoundedRectangle, ничего не произойдет. Теперь давайте настроим наше contextMenu, добавим кнопки и придадим им какое-то назначение.
Добавьте две переменные состояния, которые будут изменяться в действиях contextMenu:
@State private var text: String = "Green" @State private var color: Color = .green
Измените RoundedRectangle и Text следующим образом:
RoundedRectangle(cornerRadius: 10) .frame(width: 100, height: 100) .foregroundColor(color) .overlay { Text(text) .foregroundColor(.white) .font(.title2) .cornerRadius(10) .shadow(radius: 3.5) .frame(width: 100, height: 100) }
Наконец, добавьте приведенный ниже код в свой модификатор contextMenu:
.contextMenu { //Our context Menu actions selection Button(action:{ self.text = "Red" self.color = .red }){ Text("Set Red") } Button(action:{ self.text = "Green" self.color = .green }){ Text("Set Green") } // }
Теперь, когда мы долго нажимаем на наш прямоугольник, мы, наконец, видим наше контекстное меню и при нажатии на наши кнопки текст и цвет прямоугольника меняются!
Я знаю, что вы шокированы тем, насколько замечательными и простыми в реализации являются contextMenu, но подождите, пока вы не увидите, как вы можете использовать предварительный просмотр в своих интересах, чтобы преобразовать свой элемент и отобразить дополнительную информацию.
В нашем предварительном просмотре мы будем изменять размер RoundedRectangle вместе с отображением HEX-кода для текущего представленного цвета. Для этого измените код следующим образом:
@State private var currentColorHex: String = "33C758" func getHexCode(color: Color) -> String { let ciColor = CIColor(color: UIColor(color)) let alpha = ciColor.alpha let red = ciColor.red let blue = ciColor.blue let green = ciColor.green // Create a hex code string from the RGB components let hexCode = String(format: "%02X%02X%02X", Int(red * 255), Int(green * 255), Int(blue * 255)) print(hexCode) return hexCode }
Здесь мы объявляем новую переменную состояния, которая содержит наш шестнадцатеричный код, а также устанавливаем функцию, которая использует CIColor для получения нашего RGB-кода цветов, который мы затем будем использовать для форматирования его в шестнадцатеричный код.
После изменения действий наших кнопок следующим образом:
//Our context Menu actions selection Button(action:{ self.text = "Red" self.color = .red currentColorHex = getHexCode(color: color) }){ Text("Set Red") } Button(action:{ self.text = "Green" self.color = .green currentColorHex = getHexCode(color: color) }){ Text("Set Green") } //
Вот результат, который мы получаем при печати нашего HEX-кода, и да, я проверил значения, и они действительны.
Теперь давайте, наконец, воспользуемся предварительным просмотром нашего contextMenu! в iOS 16 у contextMenu есть специальный предварительный просмотр свойств, который делает настройку пользовательского предварительного просмотра несправедливо простой. Вот что вам нужно сделать, чтобы реализовать предварительный просмотр в iOS 16:
- Поскольку мы не используем пользовательскую ячейку для определения ширины и высоты, мы будем использовать функцию @ViewBuilder для установки размера RoundedRectangles:
@ViewBuilder func CustomRectangle(size: CGSize, isPreview: Bool) -> some View { RoundedRectangle(cornerRadius: 10) .frame(width: size.width, height: size.height) .foregroundColor(color) .overlay { VStack(alignment: .leading, spacing: 5) { Text(text) .foregroundColor(.white) .font(isPreview ? .title: .title2) .cornerRadius(10) .shadow(radius: 3.5) .frame(width: 100, height: 40) if isPreview { Text(currentColorHex) .foregroundColor(.white.opacity(0.75)) .font(.title2) .cornerRadius(10) .shadow(radius: 3.5) .frame(width: 100, height: 40) } } } }
2. Измените ContentView следующим образом:
var body: some View { CustomRectangle(size: CGSize(width: 100, height: 100), isPreview: false) .contextMenu { //Our context Menu actions selection Button(action:{ self.text = "RED" self.color = .red currentColorHex = getHexCode(color: color) }){ Text("Set Red") } Button(action:{ self.text = "GREEN" self.color = .green currentColorHex = getHexCode(color: color) }){ Text("Set Green") } // } preview: { CustomRectangle(size: CGSize(width: 250, height: 150), isPreview: true) } }
Теперь, когда мы создадим наш проект, это то, что мы должны увидеть:
Как видите, contextMenu работает как положено. Давайте теперь посмотрим, что нам нужно сделать, чтобы реализовать ту же функциональность в iOS 15.
Предварительный просмотр ContextMenu в iOS 15
Сначала давайте создадим новый файл Swift с именем ContextMenuWithPreview, который будет содержать все наши расширения, необходимые для предварительного просмотра. Вот шаги, которые мы будем выполнять:
- Добавьте скрытое наложение
UIContextMenuInteraction
. - Обеспечьте предварительный просмотр в
previewProvider
и действия вactionProvider
. - Используйте
@ViewBuilder
, чтобы упростить объявление предварительного просмотра.
import SwiftUI // MARK: - Custom Menu Context Implementation struct PreviewContextMenu<Content: View> { let size: CGSize var color: Binding<Color> var text: Binding<String> let destination: Content let actionProvider: UIContextMenuActionProvider? init(size: CGSize, color: Binding<Color>, text: Binding<String>, destination: Content, actionProvider: UIContextMenuActionProvider? = nil) { self.size = size self.destination = destination self.actionProvider = actionProvider self.color = color self.text = text } } // UIView wrapper with UIContextMenuInteraction struct PreviewContextView<Content: View>: UIViewRepresentable { let menu: PreviewContextMenu<Content> let didCommitView: () -> Void func makeUIView(context: Context) -> UIView { let view = UIView() view.backgroundColor = .clear let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator) view.addInteraction(menuInteraction) return view } func updateUIView(_ uiView: UIView, context: Context) { } func makeCoordinator() -> Coordinator { return Coordinator(menu: self.menu, didCommitView: self.didCommitView) } class Coordinator: NSObject, UIContextMenuInteractionDelegate { let menu: PreviewContextMenu<Content> let didCommitView: () -> Void init(menu: PreviewContextMenu<Content>, didCommitView: @escaping () -> Void) { self.menu = menu self.didCommitView = didCommitView } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { let previewProvider: () -> UIViewController? = { let viewController = UIHostingController(rootView: self.menu.destination) viewController.preferredContentSize = self.menu.size return viewController } return UIContextMenuConfiguration(identifier: nil, previewProvider: previewProvider, actionProvider: self.menu.actionProvider) } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { animator.addCompletion(self.didCommitView) } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { let parameters = UIPreviewParameters() parameters.backgroundColor = .clear return UITargetedPreview(view: interaction.view!, parameters: parameters) } func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { let parameters = UIPreviewParameters() parameters.backgroundColor = .clear return UITargetedPreview(view: interaction.view!, parameters: parameters) } } } // Add context menu modifier extension View { func contextMenu<Content: View>(_ menu: PreviewContextMenu<Content>) -> some View { self.modifier(PreviewContextViewModifier(menu: menu)) } } struct PreviewContextViewModifier<V: View>: ViewModifier { let menu: PreviewContextMenu<V> @Environment(\.presentationMode) var mode @State var isActive: Bool = false func body(content: Content) -> some View { Group { if isActive { menu.destination } else { content.overlay(PreviewContextView(menu: menu, didCommitView: { self.isActive = true })) } } } }
Теперь измените наш ContentView, добавив проверку версии для нашего CustomRectangle:
if #available(iOS 16.0, *) { CustomRectangle(size: CGSize(width: 100, height: 100), isPreview: false) .contextMenu { //Our context Menu actions selection Button(action:{ self.text = "RED" self.color = .red currentColorHex = getHexCode(color: color) }){ Text("Set Red") } Button(action:{ self.text = "GREEN" self.color = .green currentColorHex = getHexCode(color: color) }){ Text("Set Green") } // } preview: { CustomRectangle(size: CGSize(width: 250, height: 150), isPreview: true) } .previewLayout(<#T##value: PreviewLayout##PreviewLayout#>) } else { // Fallback on earlier versions }
Вот что вы должны добавить в замыкание с помощью // Fallback в более ранних версиях:
// Fallback on earlier versions CustomRectangle(size: CGSize(width: 100, height: 100), isPreview: false) .contextMenu(PreviewContextMenu(size: CGSize(width: 250, height: 150), color: $color, text: $text, destination: CustomRectangle(size: CGSize(width: 250, height: 150), isPreview: true), actionProvider: { items in return UIMenu(title: "", children: [greenAction, redAction]) }))
Когда дело доходит до добавления действий, мы не можем просто установить новую кнопку в предварительном просмотре. Что нам нужно сделать, так это создать отдельный UIActions:
Объявите их в теле нашего contentView:
let greenAction = UIAction( title: "Set Green", identifier: nil, handler: { _ in text = "GREEN" color = .green currentColorHex = getHexCode(color: color) } ) let redAction = UIAction( title: "Set Red", identifier: nil, handler: { _ in text = "RED" color = .red currentColorHex = getHexCode(color: color) } )
Теперь давайте запустим наш проект!
Хотя кажется, что все работает нормально, мы ясно видим, что предварительный просмотр не обновляется при изменении цветов. Причина этого в том, что CustomRectange в нашем предварительном просмотре установлен и не обновляется после этого. В то время как наш пункт назначения взят после завершения действия по изменению цвета. Чтобы решить проблему, нам нужно будет сделать следующее:
- Объявите наш пользовательский прямоугольник в пользовательском классе
- Установите значения Binding для нашего color, text и hexCode.
Создайте новый класс с именем CustomRectangle и вставьте следующий код:
struct CustomRectangle: View { @State var size: CGSize @State var isPreview: Bool @Binding var color: Color @Binding var text: String @Binding var currentColorHex: String var body: some View { RoundedRectangle(cornerRadius: 10) .frame(width: size.width, height: size.height) .foregroundColor(color) .overlay { VStack(alignment: .leading, spacing: 5) { Text(text) .foregroundColor(.white) .font(isPreview ? .title: .title2) .cornerRadius(10) .shadow(radius: 3.5) .frame(width: 100, height: 40) if isPreview { Text(currentColorHex) .foregroundColor(.white.opacity(0.75)) .font(.title2) .cornerRadius(10) .shadow(radius: 3.5) .frame(width: 100, height: 40) } } } } }
Затем измените наш ContentView, установив отсутствующие значения нашего нового CustomRectangle:
struct ContentView: View { @State private var text: String = "GREEN" @State private var color: Color = .green @State private var currentColorHex: String = "33C758" var body: some View { let greenAction = UIAction( title: "Set Green", identifier: nil, handler: { _ in text = "GREEN" color = .green currentColorHex = getHexCode(color: color) } ) let redAction = UIAction( title: "Set Red", identifier: nil, handler: { _ in text = "RED" color = .red currentColorHex = getHexCode(color: color) } ) if #available(iOS 16.0, *) { CustomRectangle(size: CGSize(width: 100, height: 100), isPreview: false, color: $color, text: $text, currentColorHex: $currentColorHex) .contextMenu { //Our context Menu actions selection Button(action:{ self.text = "RED" self.color = .red currentColorHex = getHexCode(color: color) }){ Text("Set Red") } Button(action:{ self.text = "GREEN" self.color = .green currentColorHex = getHexCode(color: color) }){ Text("Set Green") } // } preview: { CustomRectangle(size: CGSize(width: 250, height: 150), isPreview: true, color: $color, text: $text, currentColorHex: $currentColorHex) } } else { // Fallback on earlier versions CustomRectangle(size: CGSize(width: 100, height: 100), isPreview: false, color: $color, text: $text, currentColorHex: $currentColorHex) .contextMenu(PreviewContextMenu(size: CGSize(width: 250, height: 150), color: $color, text: $text, destination: CustomRectangle(size: CGSize(width: 250, height: 150), isPreview: true, color: $color, text: $text, currentColorHex: $currentColorHex), actionProvider: { items in return UIMenu(title: "", children: [greenAction, redAction]) })) } } func getHexCode(color: Color) -> String { let ciColor = CIColor(color: UIColor(color)) let red = ciColor.red let blue = ciColor.blue let green = ciColor.green // Create a hex code string from the RGB components let hexCode = String(format: "%02X%02X%02X", Int(red * 255), Int(green * 255), Int(blue * 255)) print(hexCode) return hexCode } }
Теперь, когда мы запускаем наш проект, все должно работать отлично, как и предполагалось:
Поздравляем! Теперь вы знаете, как реализовать Custom ContextView с Preview и Destination в iOS 15.
Спасибо, что прочитали эту статью!
Вы можете найти исходный код этого проекта и многих других интересных проектов на моем github: https://github.com/AisultanAskarov
Следите за моим твиттером, где я также публикую небольшие, но очень полезные фрагменты кода: https://twitter.com/aisultanios