Я реализую пользовательскую ссылку NavigationLink очень с именем MenuItem и хочу повторно использовать ее в проекте. Это структура, соответствующая View и реализующая var body : some View, которая содержит NavigationLink.
Мне нужно как-то сохранить представление, которое должно быть представлено NavigationLink в теле MenuItem, но пока не удалось этого сделать.
Я определил destinationView в теле MenuItem как some View и попробовал два инициализатора:
Это казалось слишком простым:
struct MenuItem: View {
private var destinationView: some View
init(destinationView: View) {
self.destinationView = destinationView
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
Протокол --> Ошибка: «Просмотр» может использоваться только как общее ограничение, поскольку оно имеет требования к собственному или связанному типу.
2-я попытка:
struct MenuItem: View {
private var destinationView: some View
init<V>(destinationView: V) where V: View {
self.destinationView = destinationView
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
--> Ошибка: Невозможно присвоить значение типа "V" типу "некоторое представление".
Последняя попытка:
struct MenuItem: View {
private var destinationView: some View
init<V>(destinationView: V) where V: View {
self.destinationView = destinationView as View
}
var body : some View {
// Here I'm passing destinationView to NavigationLink...
}
}
--> Ошибка: Невозможно присвоить значение типа "Представление" для типа "Некоторое представление".
Я надеюсь, что кто-то может помочь мне. Должен быть способ, если NavigationLink может принимать некоторое представление в качестве аргумента. Спасибо д
Привет @dfd! Спасибо за ответ; D Я обновил первый абзац, чтобы лучше отразить то, что я пытался сделать, а именно создать альтернативу NavigationLink под названием MenuItem. @rraphael дал правильный ответ, и теперь все работает как положено. Дженерики — важное ключевое слово для дальнейшего поиска.





Вы должны сделать общий параметр частью MenuItem:
struct MenuItem<Content: View>: View {
private var destinationView: Content
init(destinationView: Content) {
self.destinationView = destinationView
}
var body : some View {
// ...
}
}
Большое спасибо! Я все еще новичок в дженериках, поэтому приятно видеть, что на самом деле это не так сложно. Таким образом, мы в основном говорим: «Эй, есть MenuItem, который содержит объект, соответствующий View. Мы назовем его тип Content. destinationView относится к этому типу, и он нам нужен во время init()». Примечание для тех, кто читает это позже: вы можете удалить init(), так как он синтезирован для вас ;D
Единственное, что с этим связано, это то, что targetView не обновляется, когда Content обновляется ObservedObject. Есть идеи?
Apple использует конструкторы функций. Существует предопределенный под названием ViewBuilder. Сделайте его последним аргументом или единственным аргументом вашего метода init для MenuItem, например:
..., @ViewBuilder builder: @escaping () -> Content)
Назначьте его свойству, определенному примерно так:
let viewBuilder: () -> Content
Затем, когда вы хотите отобразить ваши переданные представления, просто вызовите функцию следующим образом:
HStack {
viewBuilder()
}
Вы сможете использовать свой новый вид следующим образом:
MenuItem {
Image("myImage")
Text("My Text")
}
Это позволит вам передать до 10 просмотров и использовать условия if и т. д., хотя, если вы хотите, чтобы это было более ограничивающим, вам придется определить свой собственный построитель функций. Я этого не делал, так что вам придется погуглить.
У меня есть ошибка с этим, в MenuItem есть ForEach, управляемый массивом ObservedObject. Когда массив обновляется в ObservedObject, ForEach не выполняет повторную визуализацию своего содержимого. Принимая во внимание тот же код, когда он не помещен в содержимое MenuItem. Есть идеи? То же самое со всем содержимым в MenuItem, ни одно из них не перерисовывается при обновлении содержимого ObservedObject.
Содержимое MenuItem является закрытием, поэтому массив внутри него является копией, вы можете попробовать изменить определение вашего MenuItem, чтобы он принимал ваш массив ObservedObject в качестве аргумента, а затем передал его при вызове вашего viewBuilder.
Чтобы это сработало, вам нужно добавить Content в качестве общего типа, см. Ответ raphael. struct MenuItem<Content:View>: View { }
Вы можете создать свой собственный вид следующим образом:
struct ENavigationView<Content: View>: View {
let viewBuilder: () -> Content
var body: some View {
NavigationView {
VStack {
viewBuilder()
.navigationBarTitle("My App")
}
}
}
}
struct ENavigationView_Previews: PreviewProvider {
static var previews: some View {
ENavigationView {
Text("Preview")
}
}
}
С использованием:
struct ContentView: View {
var body: some View {
ENavigationView {
Text("My Text")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
В ContentView у меня есть ObservedObject, который динамически обновляет содержимое массива ForEach. Но когда я помещаю этот foreach в ENavigationView Content внутри ContentView, этот ForEach больше не перерисовывается при обновлении ObservedObject. Есть идеи? Это то же самое для всего контента в ENavigationView в ContentView.
Спасибо, это простой пример. Привязки, переданные в представление, созданное построителем представлений, обновляются должным образом.
Вы можете передать NavigationLink (или любой другой виджет представления) в качестве переменной в подпредставление следующим образом:
import SwiftUI
struct ParentView: View {
var body: some View {
NavigationView{
VStack(spacing: 8){
ChildView(destinationView: Text("View1"), title: "1st")
ChildView(destinationView: Text("View2"), title: "2nd")
ChildView(destinationView: ThirdView(), title: "3rd")
Spacer()
}
.padding(.all)
.navigationBarTitle("NavigationLinks")
}
}
}
struct ChildView<Content: View>: View {
var destinationView: Content
var title: String
init(destinationView: Content, title: String) {
self.destinationView = destinationView
self.title = title
}
var body: some View {
NavigationLink(destination: destinationView){
Text("This item opens the \(title) view").foregroundColor(Color.black)
}
}
}
struct ThirdView: View {
var body: some View {
VStack(spacing: 8){
ChildView(destinationView: Text("View1"), title: "1st")
ChildView(destinationView: Text("View2"), title: "2nd")
ChildView(destinationView: ThirdView(), title: "3rd")
Spacer()
}
.padding(.all)
.navigationBarTitle("NavigationLinks")
}
}
Я действительно изо всех сил пытался заставить свою работать над расширением View. Полная информация о том, как его вызывать, представлена в здесь.
Расширение для View (с использованием дженериков) — не забудьте import SwiftUI:
extension View {
/// Navigate to a new view.
/// - Parameters:
/// - view: View to navigate to.
/// - binding: Only navigates when this condition is `true`.
func navigate<SomeView: View>(to view: SomeView, when binding: Binding<Bool>) -> some View {
modifier(NavigateModifier(destination: view, binding: binding))
}
}
// MARK: - NavigateModifier
fileprivate struct NavigateModifier<SomeView: View>: ViewModifier {
// MARK: Private properties
fileprivate let destination: SomeView
@Binding fileprivate var binding: Bool
// MARK: - View body
fileprivate func body(content: Content) -> some View {
NavigationView {
ZStack {
content
.navigationBarTitle("")
.navigationBarHidden(true)
NavigationLink(destination: destination
.navigationBarTitle("")
.navigationBarHidden(true),
isActive: $binding) {
EmptyView()
}
}
}
}
}
Выдающийся. Это элегантная реализация настройки представления. Он также объединяет скрытие панели навигации в одном месте, а не повторяет ее во всех представлениях, и выполняет настройку, такую как скрытие индикатора раскрытия в этом случае, когда он заключен в список.
Подводя итог всему, что я прочитал здесь, и решению, которое сработало для меня:
struct ContainerView<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
content
}
}
Это позволяет не только ставить внутрь простые View, но и, благодаря @ViewBuilder, использовать блоки if-else и switch-case:
struct SimpleView: View {
var body: some View {
ContainerView {
Text("SimpleView Text")
}
}
}
struct IfElseView: View {
var flag = true
var body: some View {
ContainerView {
if flag {
Text("True text")
} else {
Text("False text")
}
}
}
}
struct SwitchCaseView: View {
var condition = 1
var body: some View {
ContainerView {
switch condition {
case 1:
Text("One")
case 2:
Text("Two")
default:
Text("Default")
}
}
}
}
Бонус: Если вам нужен жадный контейнер, который будет претендовать на все возможное пространство (в отличие от контейнера выше, который претендует только на пространство, необходимое для его подвидов), вот он:
struct GreedyContainerView<Content: View>: View {
@ViewBuilder let content: Content
var body: some View {
Color.clear
.overlay(content)
}
}
Если вам нужен инициализатор в вашем представлении, вы также можете использовать @ViewBuilder для параметра. Даже для нескольких параметров, если вы:
init(@ViewBuilder content: () -> Content) {…}
Идеальное решение здесь. Просто хочу отметить, что я заставил это работать и на MacOS, используя ContentView вместо Content.
спасибо :) хм, Content - это просто имя общего свойства, оно не должно приводить к тому, что что-то работает или не работает. Я только что проверил это решение, и оно отлично работает на macOS без каких-либо изменений.
Именно то, что я искал, отличное объяснение - спасибо! +1
Принятый ответ приятный и простой. Синтаксис стал еще чище с iOS 14 + macOS 11:
struct ContainerView<Content: View>: View {
@ViewBuilder var content: Content
var body: some View {
content
}
}
Затем продолжайте использовать его следующим образом:
ContainerView{
...
}
В качестве альтернативы вы можете использовать расширение статической функции. Например, я делаю расширение titleBar для Text. Это упрощает повторное использование кода.
В этом случае вы можете передать оболочку @Viewbuilder с закрытием представления, возвращающим пользовательский тип, соответствующий представлению. Например:
import SwiftUI
extension Text{
static func titleBar<Content:View>(
titleString:String,
@ViewBuilder customIcon: ()-> Content
)->some View {
HStack{
customIcon()
Spacer()
Text(titleString)
.font(.title)
Spacer()
}
}
}
struct Text_Title_swift_Previews: PreviewProvider {
static var previews: some View {
Text.titleBar(titleString: "title",customIcon: {
Image(systemName: "arrowshape.turn.up.backward")
})
.previewLayout(.sizeThatFits)
}
}
Мне трудно "визуализировать" вашу проблему. Дайте мне знать, где я ошибаюсь. У вас есть одно представление под названием
MenuItem... оно является частью другого представления, которое является пунктом назначенияNavigationLink? В том, что все? Если да, то почему бы просто не создатьMainMenuпредставление, которое имеетMenuItemпросмотры и является целью вашегоNavigationLink? Обновлено: Не могли бы вы привести «конкретный» пример того, что вы пытаетесь сделать словами? Я думаю, что меня смущает? (Кстати, хороший вопрос. Я просто не думаю, что понимаю, что вы на самом деле хотите для вывода.)