Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Fix tab content view recreation #24

Open
wants to merge 1 commit into
base: release/2.0.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions Example/Example/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,31 @@ import SwiftUI
import TabBar

struct ContentView: View {

private enum Item: Int, Tabbable {
case first = 0
case second
case third

var icon: String {
switch self {
case .first: return "house"
case .second: return "magnifyingglass"
case .third: return "person"
case .first: "house"
case .second: "magnifyingglass"
case .third: "person"
}
}

var title: String {
switch self {
case .first: return "First"
case .second: return "Second"
case .third: return "Third"
case .first: "First"
case .second: "Second"
case .third: "Third"
}
}
}

@State private var selection: Item = .first
@State private var visibility: TabBarVisibility = .visible

var body: some View {
TabBar(selection: $selection, visibility: $visibility) {
Button {
Expand All @@ -61,15 +60,32 @@ struct ContentView: View {
Text("Hide/Show TabBar")
}
.tabItem(for: Item.first)
Text("Second")

TextWrapper()
.tabItem(for: Item.second)
Text("Third")

TextWrapper()
.tabItem(for: Item.third)
}
.tabBar(style: CustomTabBarStyle())
.tabItem(style: CustomTabItemStyle())
.onChange(of: selection) { newValue in
print("selection changed:", newValue)
}
Comment on lines +72 to +74
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove this print statement, as it doesn't add any significant value here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for the purpose of testing, can remove it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, remove it please.

}
}

struct TextWrapper: View {
@State var string: String = UUID().uuidString

var body: some View {
Text(string)
.onTapGesture {
string = UUID().uuidString
}
.onAppear(perform: {
print("onAppear:", string)
})
}
}
Comment on lines +78 to 90
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please clarify the purpose of this wrapper? I didn't quite understand it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for the purpose of testing recreation and lazy loading, no other meaning.
If you don't like it, I can minimize the PR.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be better revert these changes, as Example project most likely will be updated later.


Expand Down
8 changes: 4 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
// swift-tools-version:5.2
// swift-tools-version:5.3

import PackageDescription

let package = Package(
name: "TabBar",
platforms: [
.iOS(.v13)
.iOS(.v14),
],
products: [
.library(
name: "TabBar",
targets: ["TabBar"]
)
),
],
targets: [
.target(name: "TabBar")
.target(name: "TabBar"),
]
)
29 changes: 26 additions & 3 deletions Sources/TabBar/Common/Preferences/TabBarSelection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,35 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import Combine
import SwiftUI

class TabBarSelection<TabItem: Tabbable>: ObservableObject {
@Binding var selection: TabItem

private var cancelSet = Set<AnyCancellable>()

@Published var selection: TabItem {
didSet {
loadedItems.insert(selection)
}
}

@Published var items: [TabItem] = [] {
didSet {
loadedItems = [selection]
}
}

@Published var loadedItems = Set<TabItem>()

init(selection: Binding<TabItem>) {
self._selection = selection
self.selection = selection.wrappedValue
loadedItems.insert(selection.wrappedValue)

$selection.sink { item in
DispatchQueue.main.async {
selection.wrappedValue = item
}
}
.store(in: &cancelSet)
}
}
18 changes: 10 additions & 8 deletions Sources/TabBar/Common/Preferences/TabBarViewModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,31 @@ import SwiftUI

struct TabBarViewModifier<TabItem: Tabbable>: ViewModifier {
@EnvironmentObject private var selectionObject: TabBarSelection<TabItem>

let item: TabItem

func body(content: Content) -> some View {
Group {
if self.item == self.selectionObject.selection {
if selectionObject.loadedItems.contains(item) {
content
.opacity(item == selectionObject.selection ? 1 : 0)
} else {
Color.clear
}
}
.preference(key: TabBarPreferenceKey.self, value: [self.item])
.environmentObject(selectionObject)
.preference(key: TabBarPreferenceKey.self, value: [item])
}
}

extension View {
public extension View {
/**
A function that is used to associated view with the passed item.

Use this function to associate view with the specific item
of the `TabBar`.
*/
public func tabItem<TabItem: Tabbable>(for item: TabItem) -> some View {
return self.modifier(TabBarViewModifier(item: item))
func tabItem(for item: some Tabbable) -> some View {
modifier(TabBarViewModifier(item: item))
}
}
95 changes: 45 additions & 50 deletions Sources/TabBar/View/TabBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,135 +25,130 @@ import SwiftUI

/**
`TabBar` – highly customizable tab bar for your SwiftUI application.

By using this component you will be able to add a view that
switches between multiple child views using interactive user
interface elements.

`TabBar` can be easily customized. You have to conform
to `TabBarStyle` and `TabItemStyle` to customize bar
and item respectively. To apply customization you have to inject
them to tab bar using `tabBar(style:)` for bar
and `tabItem(style:)` for item.

Usage:

```
TabBar(selection: $selection) { }
.tabBar(style: CustomTabBarStyle())
.tabItem(style: CustomTabItemStyle())
```
*/
public struct TabBar<TabItem: Tabbable, Content: View>: View {

private let selectedItem: TabBarSelection<TabItem>
private let content: Content

private var tabItemStyle : AnyTabItemStyle
private var tabBarStyle : AnyTabBarStyle

@State private var items: [TabItem]

@StateObject private var selectedItem: TabBarSelection<TabItem>
private let content: () -> Content
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storing the closure as a parameter rather than invoking it immediately could lead to performance issues, as it disrupts the memoization process of SwiftUI.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, delayed closures perform better in practice. SwiftUI needs to construct View instances to compare function signatures, resulting in a higher frequency of View initialization compared to the View body getter.

Copy link
Owner

@onl1ner onl1ner Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storing closures in a parameter makes the View (in this case TabBar) to re-evaluate on every @State change of the on the closure provider side, even on unrelated one, because closures are not Equatable and SwiftUI cannot check if the return value of this closure changed or not.


private var tabItemStyle: AnyTabItemStyle
private var tabBarStyle: AnyTabBarStyle

@Binding private var visibility: TabBarVisibility

/**
Creates a tab bar components with given
bindings to selection and visibility.

Provided views in the `content` closure
will be recognized as a tab bar item only
if they have `tabItem(for:)` applied on them.
*/
public init(
selection: Binding<TabItem>,
visibility: Binding<TabBarVisibility> = .constant(.visible),
@ViewBuilder content: () -> Content
@ViewBuilder content: @escaping () -> Content
) {
self.tabItemStyle = .init(itemStyle: DefaultTabItemStyle())
self.tabBarStyle = .init(barStyle: DefaultTabBarStyle())

self.selectedItem = .init(selection: selection)
self.content = content()

self._items = .init(initialValue: .init())
self._visibility = visibility
tabItemStyle = .init(itemStyle: DefaultTabItemStyle())
tabBarStyle = .init(barStyle: DefaultTabBarStyle())

_selectedItem = .init(wrappedValue: .init(selection: selection))
self.content = content
_visibility = visibility
}

private var tabItems: some View {
HStack {
ForEach(self.items, id: \.self) { item in
self.tabItemStyle.tabItem(
ForEach(selectedItem.items, id: \.self) { item in
tabItemStyle.tabItem(
icon: item.icon,
selectedIcon: item.selectedIcon,
title: item.title,
isSelected: self.selectedItem.selection == item
isSelected: selectedItem.selection == item
)
.onTapGesture {
self.selectedItem.selection = item
self.selectedItem.objectWillChange.send()
selectedItem.selection = item
}
}
.frame(maxWidth: .infinity)
}
}

public var body: some View {
ZStack {
self.content
content()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.environmentObject(self.selectedItem)
.environmentObject(selectedItem)

GeometryReader { geometry in
VStack {
Spacer()
self.tabBarStyle.tabBar(with: geometry) {
.init(self.tabItems)

tabBarStyle.tabBar(with: geometry) {
.init(tabItems)
}
}
.edgesIgnoringSafeArea(.bottom)
.visibility(self.visibility)
.visibility(visibility)
}
}
.onPreferenceChange(TabBarPreferenceKey.self) { value in
self.items = value
if value != selectedItem.items {
selectedItem.items = value
}
}
}

}

extension TabBar {
public extension TabBar {
/**
A function that is used to apply tab item's style on `TabBar`.

By passing the instance of object that conforms to `TabItemStyle`
protocol `TabBar` will use this style for its items.

- Parameters:
- style: Item style that should be applied to `TabBar`.

- Returns:
`TabBar` with applied item style.
*/
public func tabItem<ItemStyle: TabItemStyle>(style: ItemStyle) -> Self {
func tabItem(style: some TabItemStyle) -> Self {
var _self = self
_self.tabItemStyle = .init(itemStyle: style)
return _self
}

/**
A function that is used to apply tab bar's style on `TabBar`.

By passing the instance of object that conforms to `TabBarStyle`
protocol `TabBar` will apply this style to its bar.

- Parameters:
- style: Bar style that should be applied to `TabBar`.

- Returns:
`TabBar` with applied bar style.
*/
public func tabBar<BarStyle: TabBarStyle>(style: BarStyle) -> Self {
func tabBar(style: some TabBarStyle) -> Self {
var _self = self
_self.tabBarStyle = .init(barStyle: style)
return _self
Expand Down