From f4e193f6edb26c587368269fa828f538bf06aad2 Mon Sep 17 00:00:00 2001 From: Alperen Yakut Date: Tue, 31 Jan 2023 17:52:05 +0000 Subject: [PATCH 1/2] Use bounded value instead selectionState which prevents two-way binding --- Sources/SlidingTabView/SlidingTabView.swift | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Sources/SlidingTabView/SlidingTabView.swift b/Sources/SlidingTabView/SlidingTabView.swift index cfd54f6..51e2523 100644 --- a/Sources/SlidingTabView/SlidingTabView.swift +++ b/Sources/SlidingTabView/SlidingTabView.swift @@ -27,15 +27,6 @@ import SwiftUI @available(iOS 13.0, *) public struct SlidingTabView : View { - // MARK: Internal State - - /// Internal state to keep track of the selection index - @State private var selectionState: Int = 0 { - didSet { - selection = selectionState - } - } - // MARK: Required Properties /// Binding the selection index which will re-render the consuming view @@ -113,8 +104,7 @@ public struct SlidingTabView : View { HStack(spacing: 0) { ForEach(self.tabs, id:\.self) { tab in Button(action: { - let selection = self.tabs.firstIndex(of: tab) ?? 0 - self.selectionState = selection + self.selection = self.tabs.firstIndex(of: tab)! }) { HStack { Spacer() @@ -152,11 +142,11 @@ public struct SlidingTabView : View { // MARK: Private Helper private func isSelected(tabIdentifier: String) -> Bool { - return tabs[selectionState] == tabIdentifier + return tabs[selection] == tabIdentifier } private func selectionBarXOffset(from totalWidth: CGFloat) -> CGFloat { - return self.tabWidth(from: totalWidth) * CGFloat(selectionState) + return self.tabWidth(from: totalWidth) * CGFloat(selection) } private func tabWidth(from totalWidth: CGFloat) -> CGFloat { From ba41d521ddd79330b3405da8cf931407d5cf9a5b Mon Sep 17 00:00:00 2001 From: Alperen Yakut Date: Tue, 31 Jan 2023 18:49:59 +0000 Subject: [PATCH 2/2] Add functionality for swiping between pages. --- Package.swift | 8 +- README.md | 20 +++-- Sources/SlidingTabView/SlidingTabView.swift | 89 +++++++++++++-------- 3 files changed, 71 insertions(+), 46 deletions(-) diff --git a/Package.swift b/Package.swift index 0b07719..d6b111a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,11 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.3 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "SlidingTabView", + platforms: [.iOS(.v14)], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( @@ -12,15 +13,14 @@ let package = Package( targets: ["SlidingTabView"]), ], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), + .package(url: "https://github.com/GeorgeElsham/ViewExtractor", from: "2.0.0") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "SlidingTabView", - dependencies: [], + dependencies: ["ViewExtractor"], path: "Sources"), .testTarget( name: "SlidingTabViewTests", diff --git a/README.md b/README.md index 841f0ca..8caf714 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,10 @@ Please use Swift Package Manager to install **SlidingTabView** Just instantiate and bind it to your state. That is it! ```swift @State private var selectedTabIndex = 0 -SlidingTabView(selection: $selectedTabIndex,tabs: ["First Tab", "Second Tab"] +SlidingTabView(selection: $selectedTabIndex,tabs: ["First Tab", "Second Tab"]) { + Text("First Page") + Text("Second Page") +} ``` ## Canvas Preview @@ -21,17 +24,18 @@ struct SlidingTabConsumerView : View { @State private var selectedTabIndex = 0 var body: some View { - VStack(alignment: .leading) { - SlidingTabView(selection: self.$selectedTabIndex, tabs: ["First", "Second"]) - (selectedTabIndex == 0 ? Text("First View") : Text("Second View")).padding() - Spacer() + SlidingTabView(selection: self.$selectedTabIndex, + tabs: ["First", "Second"], + font: .body, + activeAccentColor: Color.blue, + selectionBarColor: Color.blue) { + Text("First View") + Text("Second View") } - .padding(.top, 50) - .animation(.none) } } -@available(iOS 13.0.0, *) +@available(iOS 14.0.0, *) struct SlidingTabView_Previews : PreviewProvider { static var previews: some View { SlidingTabConsumerView() diff --git a/Sources/SlidingTabView/SlidingTabView.swift b/Sources/SlidingTabView/SlidingTabView.swift index 51e2523..56a1dab 100644 --- a/Sources/SlidingTabView/SlidingTabView.swift +++ b/Sources/SlidingTabView/SlidingTabView.swift @@ -23,10 +23,11 @@ // import SwiftUI +import ViewExtractor + +@available(iOS 14.0, *) +public struct SlidingTabView: View{ -@available(iOS 13.0, *) -public struct SlidingTabView : View { - // MARK: Required Properties /// Binding the selection index which will re-render the consuming view @@ -35,7 +36,9 @@ public struct SlidingTabView : View { /// The title of the tabs let tabs: [String] - // Mark: View Customization Properties + let content: () -> Content + + // MARK: View Customization Properties /// The font of the tab title let font: Font @@ -80,7 +83,8 @@ public struct SlidingTabView : View { activeTabColor: Color = .clear, selectionBarHeight: CGFloat = 2, selectionBarBackgroundColor: Color = Color.gray.opacity(0.2), - selectionBarBackgroundHeight: CGFloat = 1) { + selectionBarBackgroundHeight: CGFloat = 1, + @ViewBuilder content: @escaping () -> Content) { self._selection = selection self.tabs = tabs self.font = font @@ -93,34 +97,36 @@ public struct SlidingTabView : View { self.selectionBarHeight = selectionBarHeight self.selectionBarBackgroundColor = selectionBarBackgroundColor self.selectionBarBackgroundHeight = selectionBarBackgroundHeight + self.content = content } // MARK: View Construction - public var body: some View { - assert(tabs.count > 1, "Must have at least 2 tabs") - - return VStack(alignment: .leading, spacing: 0) { + private var tabsView: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 0) { ForEach(self.tabs, id:\.self) { tab in - Button(action: { - self.selection = self.tabs.firstIndex(of: tab)! - }) { + Button{ + withAnimation(self.animation) { + self.selection = self.tabs.firstIndex(of: tab)! + } + } label: { HStack { Spacer() Text(tab).font(self.font) Spacer() } } - .padding(.vertical, 16) - .accentColor( - self.isSelected(tabIdentifier: tab) - ? self.activeAccentColor - : self.inactiveAccentColor) - .background( - self.isSelected(tabIdentifier: tab) - ? self.activeTabColor - : self.inactiveTabColor) + .frame(height: 52) + .accentColor( + self.isSelected(tabIdentifier: tab) + ? self.activeAccentColor + : self.inactiveAccentColor) + .background( + self.isSelected(tabIdentifier: tab) + ? self.activeTabColor + : self.inactiveTabColor) } } GeometryReader { geometry in @@ -129,7 +135,7 @@ public struct SlidingTabView : View { .fill(self.selectionBarColor) .frame(width: self.tabWidth(from: geometry.size.width), height: self.selectionBarHeight, alignment: .leading) .offset(x: self.selectionBarXOffset(from: geometry.size.width), y: 0) - .animation(self.animation) + Rectangle() .fill(self.selectionBarBackgroundColor) .frame(width: geometry.size.width, height: self.selectionBarBackgroundHeight, alignment: .leading) @@ -139,6 +145,24 @@ public struct SlidingTabView : View { } } + public var body: some View { + assert(tabs.count > 1, "Must have at least 2 tabs") + + return VStack(alignment: .leading) { + tabsView + Extract(content) { views in + TabView(selection: $selection.animation(self.animation)) { + ForEach(Array(zip(views.indices, views)), id: \.1.id) {index, view in + view.tag(index) + } + } + .tabViewStyle(.page) + .indexViewStyle(.page(backgroundDisplayMode: .never)) + + } + } + } + // MARK: Private Helper private func isSelected(tabIdentifier: String) -> Bool { @@ -156,26 +180,23 @@ public struct SlidingTabView : View { #if DEBUG -@available(iOS 13.0, *) +@available(iOS 14.0, *) struct SlidingTabConsumerView : View { @State private var selectedTabIndex = 0 var body: some View { - VStack(alignment: .leading) { - SlidingTabView(selection: self.$selectedTabIndex, - tabs: ["First", "Second"], - font: .body, - activeAccentColor: Color.blue, - selectionBarColor: Color.blue) - (selectedTabIndex == 0 ? Text("First View") : Text("Second View")).padding() - Spacer() + SlidingTabView(selection: self.$selectedTabIndex, + tabs: ["First", "Second"], + font: .body, + activeAccentColor: Color.blue, + selectionBarColor: Color.blue) { + Text("First View") + Text("Second View") } - .padding(.top, 50) - .animation(.none) } } -@available(iOS 13.0.0, *) +@available(iOS 14.0.0, *) struct SlidingTabView_Previews : PreviewProvider { static var previews: some View { SlidingTabConsumerView()