Как передать данные из дочернего представления в родительское представление в другое дочернее представление в SwiftUI?

как следует из названия, я пытаюсь передать данные из одного дочернего представления (A) в другое дочернее представление (B) через родительское представление (P).

Родительское представление выглядит так:

@State var rectFrame: CGRect = .zero 

var body: some View {
    childViewA(rectFrame: $rectFrame)
    childViewB()
}

где childViewA получает CGRect, который нужен childViewB.

childViewA выглядит так:

@Binding var rectFrame: CGRect

var body: some View {
    // Very long code where rectFrame is obtained and printed to console correctly
}

Как передать rectFrame childViewB? Все, что я пробовал до сих пор, возвращает CGRect.zero в childViewB, несмотря на то, что печатаются правильные значения как в parentView, так и в childViewA.

Чтобы попытаться передать значение childViewB, я переписал parentView следующим образом:

@State var rectFrame: CGRect = .zero 

var body: some View {
    childViewA(rectFrame: $rectFrame)
    childViewB(rectFrame: $rectFrame.value)
}

при этом childViewB имеет следующую структуру:

var rectFrame: CGRect = CGRect.zero

var body: some View {

}

Но это просто печатает CGRect.zero каждый раз.

Я недавно пробовал @ObjectBinding, но я боролся с этим, поэтому, если кто-нибудь может помочь мне с этим конкретным примером, я был бы очень благодарен.

class SourceRectBindings: BindableObject {
    let willChange = PassthroughSubject<Void, Never>()

    var sourceRect: CGRect = .zero {
        willSet {
            willChange.send()
        }
    }
}

person cyril    schedule 22.07.2019    source источник
comment
Как вы ставите рамку? Его следует устанавливать только с помощью a) PreferenceKey или b) DispatchQueue.main.async {...}, иначе изменения не будут распространены.   -  person arsenius    schedule 22.07.2019
comment
@arsenius PreferenceKey   -  person cyril    schedule 22.07.2019
comment
Может проверить свою реализацию? Я очень часто использую this. .background(GeometryBinder(rect: self.$childFrame)).   -  person arsenius    schedule 22.07.2019
comment
Кроме того, это выглядит немного подозрительно: childViewB(rectFrame: $rectFrame.value). Почему не просто self.rectFrame?   -  person arsenius    schedule 22.07.2019


Ответы (3)


Вы можете использовать EnvironmentObject для подобных вещей ...

Самое приятное в EnvironmentObject заключается в том, что всякий раз и когда вы меняете один из его переменные, он всегда будет следить за тем, чтобы везде, где эта переменная использовалась, она обновлялась.

Примечание: чтобы обновить каждую переменную, вы должны убедиться, что вы проходите через класс BindableObject / EnvironmentObject в каждом представлении, в котором вы хотите обновить его ...

SourceRectBindings:

class SourceRectBindings: BindableObject {

    /* PROTOCOL PROPERTIES */
    var willChange = PassthroughSubject<Application, Never>()

    /* Seems Like you missed the 'self' (send(self)) */
    var sourceRect: CGRect = .zero { willSet { willChange.send(self) }
}

Родитель:

struct ParentView: view {
    @EnvironmentObject var sourceRectBindings: SourceRectBindings

    var body: some View {
        childViewA()
            .environmentObject(sourceRectBindings)
        childViewB()
            .environmentObject(sourceRectBindings)
    }
}

ChildViewA:

struct ChildViewA: view {
    @EnvironmentObject var sourceRectBindings: SourceRectBindings

    var body: some View {

        // Update your frame and the environment...
        // ...will update the variable everywhere it was used

        self.sourceRectBindings.sourceRect = CGRect(x: 0, y: 0, width: 300, height: 400)
        return Text("From ChildViewA : \(self.sourceRectBindings.sourceRect)")
    }
}

ChildViewB:

struct ChildViewB: view {
    @EnvironmentObject var sourceRectBindings: SourceRectBindings

    var body: some View {
        // This variable will always be up to date
        return Text("From ChildViewB : \(self.sourceRectBindings.sourceRect)")
    }
}

ПОСЛЕДНИЕ ШАГИ, ЧТОБЫ ЗНАЧИТЬ ЭТО РАБОТАТЬ

• Перейдите в самое высокое представление, вы хотите, чтобы переменная обновлялась, это ваше родительское представление, но я обычно предпочитаю свой SceneDelegate ... так что я обычно это делаю:

let sourceRectBindings = SourceRectBindings()

• Затем в том же классе SceneDelegate перейдите туда, где я указываю свое корневое представление, и прохожу через свой EnviromentObject.

window.rootViewController = UIHostingController(
                rootView: ContentView()
                    .environmentObject(sourceRectBindings)
            )

• Затем на последнем шаге я передаю его родительскому представлению.

struct ContentView : View {

    @EnvironmentObject var sourceRectBindings: SourceRectBindings

    var body: some View {
        ParentView()
            .environmentObject(sourceRectBindings)
    }
}

Если вы запустите код именно так, вы увидите, что ваш предварительный просмотр выдает ошибки. Это потому, что ему не была передана переменная окружения. Для этого просто запустите фиктивный экземпляр вашей среды:

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(SourceRectBindings())
    }
}
#endif

Теперь вы можете использовать эту переменную, как хотите, и если вы хотите передать что-нибудь еще, кроме прямоугольника ... вы можете просто добавить переменную в класс EnvironmentObject, и вы можете получить к ней доступ и обновить

person PhillipJacobs    schedule 22.07.2019
comment
Большое удовольствие ... Это видео так хорошо объяснило это ... youtube.com/watch? v = stSB04C4iS4 & t = - person PhillipJacobs; 22.07.2019

Вы не упомянули, как ChildViewB получает свой прямоугольник. То есть использует ли он GeometryReader или любой другой источник. Какой бы метод вы ни использовали, вы можете передавать информацию вверх по иерархии двумя способами: через привязку или через настройки. А если задействована геометрия, даже предпочтения якоря.

В первом случае, не видя своей реализации, вы можете пропустить DispatchQueue.main.async {} при установке rect. Это связано с тем, что изменение должно произойти после самообновления представления. Однако я считаю это грязным трюком и использую его только при тестировании, но не в производственном коде.

Вторая альтернатива (с использованием предпочтений) является более надежным подходом (и предпочтительнее ;-). Я включу код для обоих подходов, но вы обязательно должны узнать больше о предпочтениях. Недавно я написал статью, которую вы можете найти здесь: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

Первый способ: я не рекомендую передавать значения геометрии вверх по иерархии, это повлияет на способ рисования в других представлениях. Но если вы настаиваете, вот как вы можете заставить его работать:

struct ContentView : View {
    @State var rectFrame: CGRect = .zero

    var body: some View {
        VStack {
            ChildViewA(rectFrame: $rectFrame)
            ChildViewB(rectFrame: rectFrame)
        }
    }
}

struct ChildViewA: View {
    @Binding var rectFrame: CGRect

    var body: some View {
        DispatchQueue.main.async {
            self.rectFrame = CGRect(x: 10, y: 20, width: 30, height: 40)
        }

        return Text("CHILD VIEW A")
    }
}

struct ChildViewB: View {
    var rectFrame: CGRect = CGRect.zero

    var body: some View {
        Text("CHILD VIEW B: RECT WIDTH = \(rectFrame.size.width)")
    }
}

Второй способ: использование настроек

import SwiftUI

struct MyPrefKey: PreferenceKey {
    typealias Value = CGRect

    static var defaultValue: CGRect = .zero

    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
}

struct ContentView : View {
    @State var rectFrame: CGRect = .zero

    var body: some View {
        VStack {
            ChildViewA()
            ChildViewB(rectFrame: rectFrame)
        }
        .onPreferenceChange(MyPrefKey.self) {
            self.rectFrame = $0
        }

    }
}

struct ChildViewA: View {
    var body: some View {
        return Text("CHILD VIEW A")
            .preference(key: MyPrefKey.self, value: CGRect(x: 10, y: 20, width: 30, height: 40))
    }
}

struct ChildViewB: View {
    var rectFrame: CGRect = CGRect.zero

    var body: some View {
        Text("CHILD VIEW B: RECT WIDTH = \(rectFrame.size.width)")
    }
}
person kontiki    schedule 22.07.2019
comment
Если вам нужно получить размер и положение представления, проверьте ссылку, которую я опубликовал. Примеров масса. - person kontiki; 22.07.2019
comment
Я фактически использовал ваш блог (поэтому предпочтения, чтобы ответить на ваш вопрос), чтобы получить информацию CGRect в childA, которую я успешно получаю в родительском представлении. К сожалению, кажется, что Preferences не получает правильный фрейм, если представление находится внутри списка? - person cyril; 22.07.2019
comment
@cyril Я не могу сказать, не глядя на код. Может, это проблема с координатным пространством? Или проблема с выравниванием? Если обновите вопрос, посмотрю. - person kontiki; 22.07.2019
comment
Также обратите внимание, что использование EnvironmentObject (как и в другом ответе) является возможным решением. Я не пошел по этому пути, потому что думал, что вы намеревались сохранить его в инкапсулированном виде. Но возможен и более глобальный подход. - person kontiki; 22.07.2019
comment
Возможно, это проблема с координатным пространством - если я помню, в части 2 вашего сообщения в блоге используется другой метод, который не полагается на координатные пространства. В конце концов, я мог бы попробовать - person cyril; 22.07.2019
comment
Помните, что вы также можете дать имя координатному пространству вида, и когда вы используете метод frame(in:CoordinateSpace) в GeometryReader, вы можете использовать это имя для создания прямоугольника в координатном пространстве другого вида. - person kontiki; 22.07.2019
comment
Отличное объяснение! - person Sardorbek Ruzmatov; 02.05.2021

Надеюсь, я правильно понял вашу задачу - поделиться информацией между двумя детьми. Подход, который я использовал, заключается в том, чтобы иметь переменные @Binding в обоих дочерних элементах, которые имеют одно и то же состояние, то есть создавать экземпляры обоих с помощью $ rectFrame. Таким образом, оба всегда будут иметь одно и то же значение, а не значение при создании экземпляра.

В моем случае это был массив, который обновлялся в одном дочернем элементе и отображался в списке в другом, поэтому SwiftUI позаботился о реакции на изменения. Может ваш случай не так прост.

person Michael Salmon    schedule 25.07.2019