Точно! Как я мог забыть, что каждый элемент должен быть уникальным или иметь свой ID, чтоб перерисовщик знал, какой именно элемент сменился.
SwiftUI – можно сравнить с теми же React.js / Vue.js. Там аналогично просто так нельзя создать элементы в цикле. Нужен идентификатор для каждого. «DOM должен знать, какой именно дочерний элемент должен быть перерисован»
Поэтому решение моей проблемы сводится к банальному добавлению ID в ForEach (...) {...}
.
Работающий код c ForEach (раскрыть)
import SwiftUI
struct ContentView: View {
@State var selectedView = 1
var body: some View {
TabView(selection: $selectedView) {
ForEach(0..<2, id: \.self) { index in // <- Добавлено: id: \.self
Text("\(index) View – \(self.selectedView)")
.tabItem {
Image(systemName: "\(index).circle")
Text("\(index)")
}.tag(index)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
А теперь, для чего все это было нужно? Я решил создать одну вьшку, которая формировала бы мне TabView из передаваемых данных. В целом доволен. Получилось как-то так:
TabBadgeView (раскрыть)
//
// TabBadge.swift
// AirDelivery
//
// Created by Igor on 14.06.2020.
// Copyright © 2020 i96.dev. All rights reserved.
//
import SwiftUI
// MARK: - Animating font size
// https://stackoverflow.com/questions/57270550/how-to-animate-tabbar-items-on-selection-in-swiftui
struct AnimatableSFImage: AnimatableModifier {
var size: CGFloat
var animatableData: CGFloat {
get { size }
set { size = newValue }
}
func body(content: Self.Content) -> some View {
content
.font(.system(size: size))
}
}
// MARK: - Helper extension
extension Image {
func animatingSF(size: CGFloat) -> some View {
self.modifier(AnimatableSFImage(size: size))
}
}
// MARK: - TabBadge
struct TabBadge: View {
var tabs: [TabBadgeItem]
@State var selectTab: Int = 0
struct TabBadgeItem {
var name: String
var icon: String
let view: AnyView
var badge: Int
init(name: String, icon: String, view: AnyView, badge: Int = 0) {
self.name = name
self.icon = icon
self.view = view
self.badge = badge
}
}
func genBadge(count: Int) -> some View {
if count < 1 { return AnyView(EmptyView()) }
return AnyView(
Circle()
.foregroundColor(.red)
.overlay(
Text("\(count)")
.foregroundColor(.white)
.font(Font.system(size: 14))
)
.frame(width: 20, height: 20)
)
}
var tabView: some View {
TabView(selection: $selectTab) {
ForEach(0..<self.tabs.count, id: \.self) { index in
self.tabs[index].view
.tag(index)
.tabItem {
Image(systemName: self.tabs[index].icon)
.animatingSF(size: self.selectTab == index ? 30 : 15 )
.animation(.interpolatingSpring(mass: 0.7, stiffness: 200, damping: 10, initialVelocity: 4))
Text(self.tabs[index].name)
}
}
}
}
var body: some View {
GeometryReader { geo in
ZStack(alignment: .bottomLeading) {
self.tabView
ForEach(0..<self.tabs.count, id: \.self) { index in
self.genBadge(count: (self.tabs[index].badge)).offset(
x: geo.size.width / CGFloat(self.tabs.count) * CGFloat(index) + geo.size.width / CGFloat(self.tabs.count) / 2 + 10,
y: geo.size.width <= geo.size.height ? -30 : -15
)
}
}
}
}
}
// MARK: - Previews
struct TabBadge_Previews: PreviewProvider {
static var previews: some View {
TabBadge(tabs: [
.init(name: "Направления", icon: "list.bullet", view: AnyView(Text("First"))),
.init(name: "Мероприятия", icon: "calendar", view: AnyView(Text("Second")), badge: 27),
.init(name: "Аккаунт", icon: "person", view: AnyView(Text("Third")), badge: 111),
.init(name: "Контакты", icon: "info", view: AnyView(Text("In progress..."))),
])
}
}