Как передать одно представление SwiftUI в качестве переменной в другую структуру представления

Я реализую пользовательскую ссылку 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 может принимать некоторое представление в качестве аргумента. Спасибо д

Мне трудно "визуализировать" вашу проблему. Дайте мне знать, где я ошибаюсь. У вас есть одно представление под названием MenuItem... оно является частью другого представления, которое является пунктом назначения NavigationLink? В том, что все? Если да, то почему бы просто не создать MainMenu представление, которое имеет MenuItem просмотры и является целью вашего NavigationLink? Обновлено: Не могли бы вы привести «конкретный» пример того, что вы пытаетесь сделать словами? Я думаю, что меня смущает? (Кстати, хороший вопрос. Я просто не думаю, что понимаю, что вы на самом деле хотите для вывода.)

user7014451 08.07.2019 18:20

Привет @dfd! Спасибо за ответ; D Я обновил первый абзац, чтобы лучше отразить то, что я пытался сделать, а именно создать альтернативу NavigationLink под названием MenuItem. @rraphael дал правильный ответ, и теперь все работает как положено. Дженерики — важное ключевое слово для дальнейшего поиска.

Alex 08.07.2019 20:57
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
71
2
36 795
8
Перейти к ответу Данный вопрос помечен как решенный

Ответы 8

Вы должны сделать общий параметр частью 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

Alex 08.07.2019 20:48

Единственное, что с этим связано, это то, что targetView не обновляется, когда Content обновляется ObservedObject. Есть идеи?

Othyn 09.02.2020 23:53

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.

Othyn 09.02.2020 23:52

Содержимое MenuItem является закрытием, поэтому массив внутри него является копией, вы можете попробовать изменить определение вашего MenuItem, чтобы он принимал ваш массив ObservedObject в качестве аргумента, а затем передал его при вызове вашего viewBuilder.

Nathan Day 11.02.2020 06:41

Чтобы это сработало, вам нужно добавить Content в качестве общего типа, см. Ответ raphael. struct MenuItem<Content:View>: View { }

Brett 12.03.2020 00:41

Вы можете создать свой собственный вид следующим образом:

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.

Othyn 09.02.2020 23:55

Спасибо, это простой пример. Привязки, переданные в представление, созданное построителем представлений, обновляются должным образом.

Rocket Garden 30.04.2020 09:04

Вы можете передать 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()
                }
            }
        }
    }
}

Выдающийся. Это элегантная реализация настройки представления. Он также объединяет скрытие панели навигации в одном месте, а не повторяет ее во всех представлениях, и выполняет настройку, такую ​​как скрытие индикатора раскрытия в этом случае, когда он заключен в список.

Tao-Nhan Nguyen 19.03.2020 15:38
Ответ принят как подходящий

Подводя итог всему, что я прочитал здесь, и решению, которое сработало для меня:

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.

LasaleFamine 21.12.2020 22:53

спасибо :) хм, Content - это просто имя общего свойства, оно не должно приводить к тому, что что-то работает или не работает. Я только что проверил это решение, и оно отлично работает на macOS без каких-либо изменений.

ramzesenok 22.12.2020 09:10

Именно то, что я искал, отличное объяснение - спасибо! +1

LilaQ 20.05.2021 07:24

Принятый ответ приятный и простой. Синтаксис стал еще чище с 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)
    }
}


Другие вопросы по теме