SwiftUI – TabView – отображение индекса активной вкладки при использовании ForEach(...) [Решено]

swiftui

#1

В статическом варианте (когда каждая вкладка создается “вручную”) проблем нет.

Работающий код (раскрыть)
import SwiftUI

struct ContentView: View {
	@State var selectedView = 1
	
	var body: some View {
		TabView(selection: $selectedView) {
			Text("First View – \(selectedView)") // <- Корректный индекс активной вкладки
				.tabItem {
					Image(systemName: "1.circle")
					Text("First")
			}.tag(0)
			
			Text("Second View – \(selectedView)") // <- Корректный индекс активной вкладки
				.tabItem {
					Image(systemName: "2.circle")
					Text("Second")
			}.tag(1)
		}
	}
}

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

НО если обернуть элементы TabView {...} в цикл ForEach(...), то self.selectedView не меняет свое значение:

Неработающий код (раскрыть)
import SwiftUI

struct ContentView: View {
	@State var selectedView = 1
	
	var body: some View {
		TabView(selection: $selectedView) {
			ForEach(0..<2) { index in
				Text("\(index) View – \(self.selectedView)") // <- Всегда показывает "1" (хотя index меняется)
					.tabItem {
						Image(systemName: "\(index).circle")
						Text("\(index)")
				}.tag(index)
			}
		}
	}
}

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

Есть у кого-то идеи, почему так?

З.ы. Знаю, что SwiftUI еще сырой и все такое, но неужели это тоже одна из ошибок, которую сейчас никак нельзя обойти?


#2

Доброго времени суток)

SiftUI не знать, проблема скорее всего в capture list, но это далеко не точно, возможно он захватывает последнее значение в кложуре и не слушает изменения, не знаю как скейты устроены)

Вот пример на родном))

var closureArray: [() -> ()] = []

    var i = 0
    for _ in 1...5 {
        // closureArray.append { [i] in print(i) } //чтобы было правильно рсскоментируй меня
        closureArray.append { print(i) }
        i += 1
    }

    closureArray.forEach { $0() }

Вот так можно убедиться что Стейт меняется)

    struct ContentView: View {
    @State var selectedView = 1

    var body: some View {
        TabView(selection: $selectedView) {
            ForEach(0..<2) { (index) in
                Text("\(index) View – \(self.selectedView)") // <- Всегда показывает "1" (хотя index меняется)
                    .tabItem {
                        Image(systemName: "\(self.selectedView).circle")
                        Text("\(index)")
                }.tag(index)
            }
            Text("First View – \(selectedView)") // <- Корректный индекс активной вкладки
                .tabItem {
                    Image(systemName: "\(selectedView).circle")
                    Text("First")
            }.tag(2)
        }
    }
}

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

#3

Думаю так:

  1. При создании элементов без цикла, при обновлении интерфейса, система может перерисовать каждый элемент по отдельности. И присвоит каждому индекс.
  2. Когда элементы созданы циклом, при обновлении интерфейса перерисовываются оба, тк сработает цикл один раз и создаст два элемента. Это подтверждается тем, что у вас в лейбле всегда один индекс - последний. Те при изменении стейта, система ловит уведомление и перерисовывает интерфейс (инициализирует заново). Цикл сработает и создаст снова два элемента и последний индекс перезапишет предыдущий.

Тут мне кажется не очень уместно экономить место циклом, тк вкладок и так максимум 5 вроде. Лучше уменьшить код путём создания отдельного вью с передачей туда нужных данных.

И если вам не нужно отслеживать какая вкладка выбрана, а ТабВью просто для объединения нескольких вью, то и (selection: $selectedView) можно просто удалить.


#4

Если я правильно понял, то это легко проверить,

class Counter {
    var count = 0
    
    func iter() -> Int {
        count += 1
        print("COUNT: ", count)
        return count
    }
}

Цикл срабатывает один раз


#5

Возможно, но мне кажется это всё ни к чему, создавать максимум 5 вкладок циклом :slight_smile:


#6

Ну это частный случай)))

Я не знаю как тут с кнопками, может можно циклом кнопки делать и на них такая же ситуация повториться))

В общем не суть))


#7

Точно! Как я мог забыть, что каждый элемент должен быть уникальным или иметь свой 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..."))),
		])
	}
}


#8

Логично ))) ________