Несколько кнопок SwiftUI с всплывающими окнами в поведении HStack

Когда в HStack есть несколько кнопок с всплывающими окнами, я получаю странное поведение. Каждый раз, когда вы нажимаете одну кнопку, всплывающее окно отображается правильно. Но когда вы нажимаете на второй элемент, первое всплывающее окно быстро закрывается, а затем открывается снова. Ожидаемое поведение - закрытие первого всплывающего окна и открытие второго. Xcode 12.5.1, iOS 14.5

Вот мой код:

      struct ContentView: View {

var items = ["item1", "item2", "item3"]

var body: some View {
    HStack {
        MyGreatItemView(item: items[0])
        MyGreatItemView(item: items[1])
        MyGreatItemView(item: items[2])
    }
    .padding(300)
}

struct MyGreatItemView: View {
    @State var isPresented = false
    var item: String
    
    var body: some View {
        
        Button(action: { isPresented.toggle() }) {
            Text(item)
        }
        .popover(isPresented: $isPresented) {
            PopoverView(item: item)
        }
        
    }
}

struct PopoverView: View {
    @State var item: String
    
    var body: some View {
        print("new PopoverView")
        return Text("View for \(item)")
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

}

Спасибо за любую помощь!

1 ответ

Обычно вы бы использовали , но вы получите ошибку ... даже popover(item:content:)пример в документации дает сбой.

*** Завершение работы приложения из-за неперехваченного исключения «NSGenericException», причина: «UIPopoverPresentationController (<UIPopoverPresentationController: 0x14a109890>) должен иметь не равный нулю sourceView или barButtonItem, установленный до того, как произойдет презентация».

Вместо этого я придумал использовать единственное число @State presentingItem: Item? в ContentView. Это гарантирует, что все всплывающие окна привязаны к одному и тому же State, поэтому вы полностью контролируете, какие из них представлены, а какие нет.

Но, .popover(isPresented:content:)с isPresentedаргумент ожидает. Если это правда, он присутствует, если нет, он будет отклонен. Чтобы преобразовать в Bool, просто используйте пользовательский Binding.

      Binding(
    get: { presentingItem == item }, /// present popover when `presentingItem` is equal to this view's `item`
    set: { _ in presentingItem = nil } /// remove the current `presentingItem` which will dismiss the popover
)

Затем установите presentingItemвнутри действия каждой кнопки. Это та часть, где все становится немного взломано - я добавил 0.5вторая задержка, чтобы сначала закрыть текущее всплывающее окно. В противном случае его не будет.

      if presentingItem == nil { /// no popover currently presented
    presentingItem = item /// dismiss that immediately, then present this popover
} else { /// another popover is currently presented...
    presentingItem = nil /// dismiss it first
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        presentingItem = item /// present this popover after a delay
    }
}

Полный код:

      /// make equatable, for the `popover` presentation logic
struct Item: Equatable {
    let id = UUID()
    var name: String
}

struct ContentView: View {
    
    @State var presentingItem: Item? /// the current presenting popover
    let items = [
        Item(name: "item1"),
        Item(name: "item2"),
        Item(name: "item3")
    ]

    var body: some View {
        HStack {
            MyGreatItemView(presentingItem: $presentingItem, item: items[0])
            MyGreatItemView(presentingItem: $presentingItem, item: items[1])
            MyGreatItemView(presentingItem: $presentingItem, item: items[2])
        }
        .padding(300)
    }
}

struct MyGreatItemView: View {
    @Binding var presentingItem: Item?
    let item: Item /// this view's item
    
    var body: some View {
        Button(action: {
            if presentingItem == nil { /// no popover currently presented
                presentingItem = item /// dismiss that immediately, then present this popover
            } else { /// another popover is currently presented...
                presentingItem = nil /// dismiss it first
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    presentingItem = item /// present this popover after a delay
                }
            }
        }) {
            Text(item.name)
        }
        
        /// `get`: present popover when `presentingItem` is equal to this view's `item`
        /// `set`: remove the current `presentingItem` which will dismiss the popover
        .popover(isPresented: Binding(get: { presentingItem == item }, set: { _ in presentingItem = nil }) ) {
            PopoverView(item: item)
        }
    }
}

struct PopoverView: View {
    let item: Item /// no need for @State here
    var body: some View {
        print("new PopoverView")
        return Text("View for \(item.name)")
    }
}

Результат:

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