├── .gitignore ├── AxisSegmentedViewExample ├── AxisSegmentedViewExample (tvOS) │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── App Icon & Top Shelf Image.brandassets │ │ │ ├── App Icon - App Store.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ ├── App Icon.imagestack │ │ │ │ ├── Back.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── Front.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Middle.imagestacklayer │ │ │ │ │ ├── Content.imageset │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── Top Shelf Image Wide.imageset │ │ │ │ └── Contents.json │ │ │ └── Top Shelf Image.imageset │ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── AxisSegmentedViewExample__tvOS_App.swift │ ├── ContentView.swift │ ├── CustomStyle.swift │ ├── CustomStyleView.swift │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── SegmentedListView.swift │ ├── SegmentedViewWithControl.swift │ ├── SelectionItemView.swift │ ├── ViewModel │ │ ├── BacisValue.swift │ │ ├── CapsuleValue.swift │ │ ├── JellyValue.swift │ │ ├── LineValue.swift │ │ ├── NeumorphismValue.swift │ │ ├── NormalValue.swift │ │ ├── ScaleValue.swift │ │ └── ViscosityValue.swift │ └── WithoutStyleView.swift ├── AxisSegmentedViewExample.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── AxisSegmentedView.xcscheme ├── Shared │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── image.imageset │ │ │ ├── Contents.json │ │ │ └── olimpia-campean-IkhxVv5Mkn4-unsplash.jpg │ ├── AxisSegmentedViewExampleApp.swift │ ├── Color+Extensions.swift │ ├── ContentView.swift │ ├── CustomStyle.swift │ ├── CustomStyleView.swift │ ├── SegmentedListView.swift │ ├── SegmentedViewWithControl.swift │ ├── SelectionItemView.swift │ ├── ViewModel │ │ ├── BacisValue.swift │ │ ├── CapsuleValue.swift │ │ ├── JellyValue.swift │ │ ├── LineValue.swift │ │ ├── NeumorphismValue.swift │ │ ├── NormalValue.swift │ │ ├── ScaleValue.swift │ │ └── ViscosityValue.swift │ └── WithoutStyleView.swift └── macOS │ └── macOS.entitlements ├── LICENSE ├── Markdown ├── AxisSegmentedView1.png ├── AxisSegmentedView2.png └── AxisSegmentedView3.png ├── Package.swift ├── README.md ├── Sources └── AxisSegmentedView │ ├── AxisSegmentedView.swift │ ├── Private │ ├── Model │ │ └── ASItem.swift │ ├── Modifiers │ │ └── ASItemModifier.swift │ └── ViewModel │ │ ├── ASPositionValue.swift │ │ └── ASSelectionValue.swift │ └── Public │ ├── Constants │ ├── ASConstant.swift │ └── ASLineConstant.swift │ ├── Extensions │ ├── Animation+Extensions.swift │ └── View+Extensions.swift │ ├── Modifiers │ ├── NeumorphismInnerModifier.swift │ └── NeumorphismOuterModifier.swift │ ├── Previews │ └── SegmentedViewPreview.swift │ ├── Shapes │ └── ASCurveShape.swift │ ├── Styles │ ├── ASBasicStyle.swift │ ├── ASCapsuleStyle.swift │ ├── ASJellyStyle.swift │ ├── ASLineStyle.swift │ ├── ASNeumorphismStyle.swift │ ├── ASScaleStyle.swift │ └── Movement │ │ ├── ASNormalStyle.swift │ │ └── ASViscosityStyle.swift │ └── ViewModel │ └── ASStateValue.swift └── Tests └── AxisSegmentedViewTests └── AxisSegmentedViewTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | }, 6 | "layers" : [ 7 | { 8 | "filename" : "Front.imagestacklayer" 9 | }, 10 | { 11 | "filename" : "Middle.imagestacklayer" 12 | }, 13 | { 14 | "filename" : "Back.imagestacklayer" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | } 11 | ], 12 | "info" : { 13 | "author" : "xcode", 14 | "version" : 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets" : [ 3 | { 4 | "filename" : "App Icon - App Store.imagestack", 5 | "idiom" : "tv", 6 | "role" : "primary-app-icon", 7 | "size" : "1280x768" 8 | }, 9 | { 10 | "filename" : "App Icon.imagestack", 11 | "idiom" : "tv", 12 | "role" : "primary-app-icon", 13 | "size" : "400x240" 14 | }, 15 | { 16 | "filename" : "Top Shelf Image Wide.imageset", 17 | "idiom" : "tv", 18 | "role" : "top-shelf-image-wide", 19 | "size" : "2320x720" 20 | }, 21 | { 22 | "filename" : "Top Shelf Image.imageset", 23 | "idiom" : "tv", 24 | "role" : "top-shelf-image", 25 | "size" : "1920x720" 26 | } 27 | ], 28 | "info" : { 29 | "author" : "xcode", 30 | "version" : 1 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "tv", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "tv", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "tv-marketing", 13 | "scale" : "1x" 14 | }, 15 | { 16 | "idiom" : "tv-marketing", 17 | "scale" : "2x" 18 | } 19 | ], 20 | "info" : { 21 | "author" : "xcode", 22 | "version" : 1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/AxisSegmentedViewExample__tvOS_App.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AxisSegmentedViewExample__tvOS_App.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/27. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @main 12 | struct AxisSegmentedViewExample__tvOS_App: App { 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/27. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | struct ContentView: View { 13 | 14 | @State private var tabViewSelection: Int = 0 15 | 16 | var body: some View { 17 | TabView(selection: $tabViewSelection) { 18 | SegmentedListView(axisMode: .horizontal) 19 | .tag(0) 20 | .tabItem { 21 | Image(systemName: "rectangle.arrowtriangle.2.inward") 22 | Text("Horizontal") 23 | } 24 | 25 | SegmentedListView(axisMode: .vertical) 26 | .tag(1) 27 | .tabItem { 28 | Image(systemName: "rectangle.portrait.arrowtriangle.2.inward") 29 | Text("Vertical") 30 | } 31 | 32 | WithoutStyleView() 33 | .tag(2) 34 | .tabItem { 35 | Image(systemName: "cpu") 36 | Text("Without style") 37 | } 38 | 39 | CustomStyleView() 40 | .tag(3) 41 | .tabItem { 42 | Image(systemName: "skew") 43 | Text("Custom Style") 44 | } 45 | .padding() 46 | } 47 | .navigationTitle(Text("AxisSegmentedView")) 48 | .padding() 49 | } 50 | } 51 | 52 | struct ContentView_Previews: PreviewProvider { 53 | static var previews: some View { 54 | ContentView() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/CustomStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomStyle.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/25. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | public struct CustomStyle: View { 13 | 14 | @EnvironmentObject var stateValue: ASStateValue 15 | 16 | let color: Color 17 | public init(color: Color = .purple) { 18 | self.color = color 19 | } 20 | 21 | private var selectionView: some View { 22 | RoundedRectangle(cornerRadius: 5) 23 | .fill(Color.yellow) 24 | } 25 | 26 | public var body: some View { 27 | let selectionFrame = stateValue.selectionFrame 28 | ZStack(alignment: .topLeading) { 29 | Color.clear 30 | RoundedRectangle(cornerRadius: 5) 31 | .stroke() 32 | .fill(color) 33 | .frame(width: selectionFrame.width, height: selectionFrame.height) 34 | .offset(x: selectionFrame.origin.x, y: selectionFrame.origin.y) 35 | } 36 | .animation(.easeInOut, value: stateValue.selectionIndex) 37 | } 38 | } 39 | 40 | struct CustomStyle_Previews: PreviewProvider { 41 | static var previews: some View { 42 | SegmentedViewPreview(constant: .init()) { 43 | CustomStyle() 44 | .preferredColorScheme(.dark) 45 | } 46 | .frame(height: 44) 47 | .padding(.horizontal, 16) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/CustomStyleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomStyleView.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/26. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | struct CustomStyleView: View { 13 | 14 | @State private var selection: Int = 0 15 | 16 | var body: some View { 17 | AxisSegmentedView(selection: $selection, constant: .init()) { 18 | Image(systemName: "align.horizontal.left") 19 | .itemTag(0, selectArea: 0) { 20 | SelectionItemView("align.horizontal.left.fill") 21 | } 22 | Image(systemName: "align.horizontal.right") 23 | .itemTag(1, selectArea: 560) { 24 | SelectionItemView("align.horizontal.right.fill") 25 | } 26 | Image(systemName: "align.vertical.top") 27 | .itemTag(2, selectArea: 0) { 28 | SelectionItemView("align.vertical.top.fill") 29 | } 30 | Image(systemName: "align.vertical.bottom") 31 | .itemTag(3, selectArea: 560) { 32 | SelectionItemView("align.vertical.bottom.fill") 33 | } 34 | } style: { 35 | CustomStyle(color: .red) 36 | } 37 | .frame(height: 80) 38 | } 39 | } 40 | 41 | struct CustomStyleView_Previews: PreviewProvider { 42 | static var previews: some View { 43 | CustomStyleView() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/SegmentedListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SegmentedListView.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | enum StyleType: String { 13 | case ASNormalStyle 14 | case ASViscosityStyle 15 | 16 | case ASBasicStyle 17 | case ASCapsuleStyle 18 | case ASJellyStyle 19 | case ASLineStyle 20 | case ASNeumorphismStyle 21 | case ASScaleStyle 22 | } 23 | 24 | 25 | struct SegmentedListView: View { 26 | 27 | let axisMode: ASAxisMode 28 | 29 | @StateObject private var normalValue: NormalValue = .init() 30 | @StateObject private var viscosityValue: ViscosityValue = .init() 31 | 32 | @StateObject private var basicValue: BasicValue = .init() 33 | @StateObject private var capsuleValue: CapsuleValue = .init() 34 | @StateObject private var jellyValue: JellyValue = .init() 35 | @StateObject private var lineValue: LineValue = .init() 36 | @StateObject private var neumorphismValue: NeumorphismValue = .init() 37 | @StateObject private var scaleValue: ScaleValue = .init() 38 | 39 | var content: some View { 40 | Group { 41 | SegmentedViewWithControl(title: "ABNormalStyle", styleType: .ASNormalStyle, axisMode: axisMode, constant: $normalValue.constant, tabs: { 42 | Group { 43 | Text("Clear") 44 | .font(.callout) 45 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 46 | .foregroundColor(Color.white.opacity(0.5)) 47 | .itemTag(0, selectArea: normalValue.selectArea0) { 48 | HStack { 49 | Image(systemName: "checkmark.circle") 50 | Text("Clear") 51 | } 52 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 53 | .font(.callout) 54 | .foregroundColor(Color.white) 55 | } 56 | Text("Confusing") 57 | .font(.callout) 58 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 59 | .foregroundColor(Color.white.opacity(0.5)) 60 | .itemTag(1, selectArea: normalValue.selectArea1) { 61 | HStack { 62 | Image(systemName: "checkmark.circle") 63 | Text("Confusing") 64 | } 65 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 66 | .font(.callout) 67 | .foregroundColor(Color.white) 68 | } 69 | } 70 | }, style: { 71 | ASNormalStyle { _ in 72 | RoundedRectangle(cornerRadius: 5) 73 | .fill(Color(hex: 0x191919)) 74 | .overlay( 75 | RoundedRectangle(cornerRadius: 5) 76 | .stroke() 77 | .fill(Color(hex: 0x282828)) 78 | ) 79 | .padding(3.5) 80 | } 81 | .background(Color(hex: 0x0B0C10)) 82 | .clipShape(RoundedRectangle(cornerRadius: 5)) 83 | }) 84 | SegmentedViewWithControl(title: "ASViscosityStyle", styleType: .ASViscosityStyle, axisMode: axisMode, constant: $viscosityValue.constant, tabs: { 85 | Group { 86 | Text("Store") 87 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 88 | .font(.callout) 89 | .foregroundColor(Color.white.opacity(0.5)) 90 | .itemTag(0, selectArea: viscosityValue.selectArea0) { 91 | Text("Store") 92 | .font(.callout) 93 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 94 | .foregroundColor(Color.white) 95 | } 96 | Text("Library") 97 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 98 | .font(.callout) 99 | .foregroundColor(Color.white.opacity(0.5)) 100 | .itemTag(1, selectArea: viscosityValue.selectArea1) { 101 | Text("Library") 102 | .font(.callout) 103 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 104 | .foregroundColor(Color.white) 105 | } 106 | Text("Downloads") 107 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 108 | .font(.callout) 109 | .foregroundColor(Color.white.opacity(0.5)) 110 | .itemTag(2, selectArea: viscosityValue.selectArea2) { 111 | Text("Downloads") 112 | .font(.callout) 113 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 114 | .foregroundColor(Color.white) 115 | } 116 | } 117 | }, style: { 118 | ASViscosityStyle { _ in 119 | Capsule() 120 | .fill(LinearGradient(colors: [Color(hex: 0x222222), Color(hex: 0x111111)], 121 | startPoint: axisMode == .horizontal ? UnitPoint.top : UnitPoint.leading, 122 | endPoint: axisMode == .horizontal ? UnitPoint.bottom : UnitPoint.trailing)) 123 | .overlay( 124 | Capsule() 125 | .stroke() 126 | .fill(Color.black) 127 | ) 128 | .padding(2) 129 | } 130 | .background(Color.black.opacity(0.2)) 131 | .clipShape(Capsule()) 132 | .innerShadow(Capsule(), radius: 1, opacity: 0.5, isDark: true) 133 | }) 134 | 135 | SegmentedViewWithControl(title: "ASBasicStyle", styleType: .ASBasicStyle, axisMode: axisMode, constant: $basicValue.constant, tabs: { 136 | Group { 137 | Text("Male") 138 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 139 | .font(.callout) 140 | .foregroundColor(Color.white.opacity(0.5)) 141 | .itemTag(0, selectArea: basicValue.selectArea0) { 142 | Text("Male") 143 | .font(.callout) 144 | .fixedSize() 145 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 146 | .foregroundColor(Color.white) 147 | } 148 | Text("Female") 149 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 150 | .font(.callout) 151 | .foregroundColor(Color.white.opacity(0.5)) 152 | .itemTag(1, selectArea: basicValue.selectArea1) { 153 | Text("Female") 154 | .font(.callout) 155 | .fixedSize() 156 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 157 | .foregroundColor(Color.white) 158 | } 159 | Text("Other") 160 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 161 | .font(.callout) 162 | .foregroundColor(Color.white.opacity(0.5)) 163 | .itemTag(2, selectArea: basicValue.selectArea2) { 164 | Text("Other") 165 | .font(.callout) 166 | .fixedSize() 167 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 168 | .foregroundColor(Color.white) 169 | } 170 | } 171 | }, style: { 172 | ASBasicStyle(backgroundColor: basicValue.backgroundColor, 173 | foregroundColor: basicValue.foregroundColor, 174 | cornerRadius: basicValue.cornerRadius, 175 | padding: basicValue.padding, 176 | isApplySelectionCornerRadius: basicValue.isApplySelectionCornerRadius, 177 | movementMode: basicValue.movementMode) 178 | }) 179 | 180 | SegmentedViewWithControl(title: "ASJellyStyle", styleType: .ASJellyStyle, axisMode: axisMode, constant: $jellyValue.constant, tabs: { 181 | Group { 182 | Image(systemName: "align.horizontal.left") 183 | .itemTag(0, selectArea: jellyValue.selectArea0) { 184 | SelectionItemView("align.horizontal.left.fill") 185 | } 186 | Image(systemName: "align.horizontal.right") 187 | .itemTag(1, selectArea: jellyValue.selectArea1) { 188 | SelectionItemView("align.horizontal.right.fill") 189 | } 190 | Image(systemName: "align.vertical.top") 191 | .itemTag(2, selectArea: jellyValue.selectArea2) { 192 | SelectionItemView("align.vertical.top.fill") 193 | } 194 | Image(systemName: "align.vertical.bottom") 195 | .itemTag(3, selectArea: jellyValue.selectArea3) { 196 | SelectionItemView("align.vertical.bottom.fill") 197 | } 198 | } 199 | }, style: { 200 | ASJellyStyle(backgroundColor: jellyValue.backgroundColor, 201 | foregroundColor: jellyValue.foregroundColor, 202 | jellyRadius: jellyValue.jellyRadius, 203 | jellyDepth: jellyValue.jellyDepth, 204 | jellyEdge: jellyValue.jellyEdge) 205 | // .overlay( 206 | // RoundedRectangle(cornerRadius: 11) 207 | // .stroke(.purple, lineWidth: 1) 208 | // .padding(1) 209 | // ) 210 | // .clipShape(RoundedRectangle(cornerRadius: 11)) 211 | }) 212 | 213 | SegmentedViewWithControl(title: "ASLineStyle", styleType: .ASLineStyle, axisMode: axisMode, constant: $lineValue.constant, tabs: { 214 | Group { 215 | Image(systemName: "align.horizontal.left") 216 | .itemTag(0, selectArea: lineValue.selectArea0) { 217 | SelectionItemView("align.horizontal.left.fill") 218 | } 219 | Image(systemName: "align.horizontal.right") 220 | .itemTag(1, selectArea: lineValue.selectArea1) { 221 | SelectionItemView("align.horizontal.right.fill") 222 | } 223 | Image(systemName: "align.vertical.top") 224 | .itemTag(2, selectArea: lineValue.selectArea2) { 225 | SelectionItemView("align.vertical.top.fill") 226 | } 227 | Image(systemName: "align.vertical.bottom") 228 | .itemTag(3, selectArea: lineValue.selectArea3) { 229 | SelectionItemView("align.vertical.bottom.fill") 230 | } 231 | } 232 | }, style: { 233 | ASLineStyle(lineColor: lineValue.lineColor, 234 | lineSmallWidth: lineValue.lineSmallWidth, 235 | lineLargeScale: lineValue.lineLargeScale, 236 | lineEdge: lineValue.lineEdge, 237 | movementMode: lineValue.movementMode) 238 | .overlay( 239 | Rectangle() 240 | .stroke() 241 | .fill(Color(hex: 0x303030)) 242 | ) 243 | }) 244 | 245 | SegmentedViewWithControl(title: "ASCapsuleStyle", styleType: .ASCapsuleStyle, axisMode: axisMode, constant: $capsuleValue.constant, tabs: { 246 | Group { 247 | Image(systemName: "align.horizontal.left") 248 | .itemTag(0, selectArea: capsuleValue.selectArea0) { 249 | SelectionItemView("align.horizontal.left.fill") 250 | } 251 | Image(systemName: "align.horizontal.right") 252 | .itemTag(1, selectArea: capsuleValue.selectArea1) { 253 | SelectionItemView("align.horizontal.right.fill") 254 | } 255 | Image(systemName: "align.vertical.top") 256 | .itemTag(2, selectArea: capsuleValue.selectArea2) { 257 | SelectionItemView("align.vertical.top.fill") 258 | } 259 | Image(systemName: "align.vertical.bottom") 260 | .itemTag(3, selectArea: capsuleValue.selectArea3) { 261 | SelectionItemView("align.vertical.bottom.fill") 262 | } 263 | } 264 | }, style: { 265 | ASCapsuleStyle(backgroundColor: capsuleValue.backgroundColor, 266 | foregroundColor: capsuleValue.foregroundColor, 267 | movementMode: capsuleValue.movementMode) 268 | }) 269 | 270 | SegmentedViewWithControl(title: "ASNeumorphismStyle", styleType: .ASNeumorphismStyle, axisMode: axisMode, constant: $neumorphismValue.constant, area: 120, tabs: { 271 | Group { 272 | Image(systemName: "align.horizontal.left") 273 | .itemTag(0, selectArea: neumorphismValue.selectArea0) { 274 | SelectionItemView("align.horizontal.left.fill") 275 | } 276 | Image(systemName: "align.horizontal.right") 277 | .itemTag(1, selectArea: neumorphismValue.selectArea1) { 278 | SelectionItemView("align.horizontal.right.fill") 279 | } 280 | Image(systemName: "align.vertical.top") 281 | .itemTag(2, selectArea: neumorphismValue.selectArea2) { 282 | SelectionItemView("align.vertical.top.fill") 283 | } 284 | Image(systemName: "align.vertical.bottom") 285 | .itemTag(3, selectArea: neumorphismValue.selectArea3) { 286 | SelectionItemView("align.vertical.bottom.fill") 287 | } 288 | } 289 | }, style: { 290 | ASNeumorphismStyle(backgroundColor: neumorphismValue.backgroundColor, 291 | foregroundColor: neumorphismValue.foregroundColor, 292 | cornerRadius: neumorphismValue.cornerRadius, 293 | padding: neumorphismValue.padding, 294 | shadowRadius: neumorphismValue.shadowRadius, 295 | shadowOpacity: neumorphismValue.shadowOpacity, 296 | isInner: neumorphismValue.isInner, 297 | movementMode: neumorphismValue.movementMode) 298 | }) 299 | 300 | SegmentedViewWithControl(title: "ASScaleStyle", styleType: .ASScaleStyle, axisMode: axisMode, constant: $scaleValue.constant, tabs: { 301 | Group { 302 | Image(systemName: "align.horizontal.left") 303 | .itemTag(0, selectArea: scaleValue.selectArea0) { 304 | SelectionItemView("align.horizontal.left.fill") 305 | } 306 | Image(systemName: "align.horizontal.right") 307 | .itemTag(1, selectArea: scaleValue.selectArea1) { 308 | SelectionItemView("align.horizontal.right.fill") 309 | } 310 | Image(systemName: "align.vertical.top") 311 | .itemTag(2, selectArea: scaleValue.selectArea2) { 312 | SelectionItemView("align.vertical.top.fill") 313 | } 314 | Image(systemName: "align.vertical.bottom") 315 | .itemTag(3, selectArea: scaleValue.selectArea3) { 316 | SelectionItemView("align.vertical.bottom.fill") 317 | } 318 | } 319 | }, style: { 320 | ASScaleStyle(backgroundColor: scaleValue.backgroundColor, 321 | foregroundColor: scaleValue.foregroundColor, 322 | cornerRadius: scaleValue.cornerRadius, 323 | minimumScale: scaleValue.minimumScale) 324 | }) 325 | } 326 | } 327 | var body: some View { 328 | ZStack { 329 | if axisMode == .horizontal { 330 | ScrollView { 331 | VStack(spacing: 20) { 332 | content 333 | } 334 | .padding(.horizontal, 5) 335 | } 336 | }else { 337 | ScrollView(.horizontal) { 338 | HStack(spacing: 20) { 339 | content 340 | } 341 | .padding(.vertical, 5) 342 | } 343 | } 344 | } 345 | .environmentObject(normalValue) 346 | .environmentObject(viscosityValue) 347 | .environmentObject(basicValue) 348 | .environmentObject(capsuleValue) 349 | .environmentObject(jellyValue) 350 | .environmentObject(lineValue) 351 | .environmentObject(neumorphismValue) 352 | .environmentObject(scaleValue) 353 | } 354 | } 355 | 356 | struct SegmentedListView_Previews: PreviewProvider { 357 | static var previews: some View { 358 | SegmentedListView(axisMode: .horizontal) 359 | .padding() 360 | .preferredColorScheme(.dark) 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/SegmentedViewWithControl.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SegmentedViewWithControl.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | struct SegmentedViewWithControl : View { 13 | 14 | @State private var isShowControlView: Bool = false 15 | @State private var selection: Int = 0 16 | @State private var maxSelectArea: CGFloat = 0 17 | 18 | @EnvironmentObject private var normalValue: NormalValue 19 | @EnvironmentObject private var viscosityValue: ViscosityValue 20 | 21 | @EnvironmentObject private var basicValue: BasicValue 22 | @EnvironmentObject private var capsuleValue: CapsuleValue 23 | @EnvironmentObject private var jellyValue: JellyValue 24 | @EnvironmentObject private var lineValue: LineValue 25 | @EnvironmentObject private var neumorphismValue: NeumorphismValue 26 | @EnvironmentObject private var scaleValue: ScaleValue 27 | 28 | let title: String 29 | let styleType: StyleType 30 | let axisMode: ASAxisMode 31 | @Binding var constant: ASConstant 32 | let area: CGFloat 33 | var tabs: () -> Tabs 34 | var style: () -> Style 35 | 36 | init(title: String, 37 | styleType: StyleType, 38 | axisMode: ASAxisMode = .horizontal, 39 | constant: Binding, 40 | area: CGFloat = 80, 41 | @ViewBuilder tabs: @escaping () -> Tabs, 42 | @ViewBuilder style: @escaping () -> Style) { 43 | self.title = title 44 | self.styleType = styleType 45 | self.axisMode = axisMode 46 | _constant = constant 47 | self.area = area 48 | self.tabs = tabs 49 | self.style = style 50 | } 51 | 52 | private var segmentedView: some View { 53 | AxisSegmentedView(selection: $selection, constant: constant, { 54 | tabs() 55 | }, style: { 56 | style() 57 | }) 58 | .font(.system(size: 20)) 59 | } 60 | 61 | private var controlView: some View { 62 | ZStack { 63 | Color(hex: 0x030303) 64 | .cornerRadius(8) 65 | ScrollView { 66 | VStack { 67 | getStyleControlView() 68 | VStack(alignment: .leading, spacing: 8) { 69 | Text("● Divide Line").opacity(0.5).font(.caption) 70 | HStack { 71 | Text("isShowSelectionLine") 72 | Spacer() 73 | Toggle(isOn: $constant.divideLine.isShowSelectionLine) {} 74 | } 75 | } 76 | .padding() 77 | 78 | VStack(alignment: .leading, spacing: 8) { 79 | Text("● Active").opacity(0.5).font(.caption) 80 | HStack { 81 | Text("ActivatedGeometryEffect") 82 | Spacer() 83 | Toggle(isOn: $constant.isActivatedGeometryEffect) {} 84 | } 85 | } 86 | .padding() 87 | } 88 | } 89 | .font(.footnote) 90 | .labelsHidden() 91 | } 92 | } 93 | 94 | var body: some View { 95 | ZStack { 96 | ZStack { 97 | if constant.axisMode == .horizontal { 98 | segmentedView 99 | .frame(height: area) 100 | }else { 101 | segmentedView 102 | .frame(width: area) 103 | } 104 | } 105 | .padding(10) 106 | } 107 | .background( 108 | GeometryReader { proxy in 109 | Color(hex: 0x15151A) 110 | .cornerRadius(8) 111 | .onAppear { 112 | self.maxSelectArea = constant.axisMode == .horizontal ? proxy.size.width * 0.5 : proxy.size.height * 0.5 113 | } 114 | } 115 | ) 116 | .onAppear { 117 | DispatchQueue.main.async { 118 | constant.axisMode = axisMode 119 | } 120 | } 121 | } 122 | 123 | private func getStyleControlView() -> some View { 124 | Group { 125 | VStack(alignment: .leading, spacing: 8) { 126 | HStack { 127 | Text("● " + title).opacity(0.5).font(.caption) 128 | Spacer() 129 | } 130 | switch styleType { 131 | case .ASBasicStyle: 132 | Group { 133 | HStack { 134 | Text("isApplySelectionCornerRadius") 135 | Spacer() 136 | Toggle(isOn: $basicValue.isApplySelectionCornerRadius) {} 137 | } 138 | HStack { 139 | Text("Movement Mode") 140 | Spacer() 141 | Picker(selection: $basicValue.movementMode) { 142 | Text("Normal").tag(ASMovementMode.normal) 143 | Text("Viscosity").tag(ASMovementMode.viscosity) 144 | } label: {} 145 | .pickerStyle(.segmented) 146 | } 147 | } 148 | case .ASCapsuleStyle: 149 | Group { 150 | HStack { 151 | Text("Movement Mode") 152 | Spacer() 153 | Picker(selection: $capsuleValue.movementMode) { 154 | Text("Normal").tag(ASMovementMode.normal) 155 | Text("Viscosity").tag(ASMovementMode.viscosity) 156 | } label: {} 157 | .pickerStyle(.segmented) 158 | } 159 | } 160 | case .ASJellyStyle: 161 | Group { 162 | HStack { 163 | Text("Jelly Edge") 164 | Spacer() 165 | Picker(selection: $jellyValue.jellyEdge) { 166 | Text("Bottom/Trailing").tag(ASEdgeMode.bottomTrailing) 167 | Text("Top/Leading").tag(ASEdgeMode.topLeading) 168 | } label: {} 169 | .pickerStyle(.segmented) 170 | } 171 | } 172 | case .ASLineStyle: 173 | Group { 174 | HStack { 175 | Text("Line Edge") 176 | Spacer() 177 | Picker(selection: $lineValue.lineEdge) { 178 | Text("Bottom/Trailing").tag(ASEdgeMode.bottomTrailing) 179 | Text("Top/Leading").tag(ASEdgeMode.topLeading) 180 | } label: {} 181 | .pickerStyle(.segmented) 182 | } 183 | HStack { 184 | Text("Movement Mode") 185 | Spacer() 186 | Picker(selection: $lineValue.movementMode) { 187 | Text("Normal").tag(ASMovementMode.normal) 188 | Text("Viscosity").tag(ASMovementMode.viscosity) 189 | } label: {} 190 | .pickerStyle(.segmented) 191 | } 192 | } 193 | case .ASNeumorphismStyle: 194 | Group { 195 | HStack { 196 | Text("isInner") 197 | Spacer() 198 | Toggle(isOn: $neumorphismValue.isInner) {} 199 | } 200 | HStack { 201 | Text("Movement Mode") 202 | Spacer() 203 | Picker(selection: $neumorphismValue.movementMode) { 204 | Text("Normal").tag(ASMovementMode.normal) 205 | Text("Viscosity").tag(ASMovementMode.viscosity) 206 | } label: {} 207 | .pickerStyle(.segmented) 208 | } 209 | } 210 | default: EmptyView() 211 | } 212 | } 213 | .padding() 214 | } 215 | } 216 | } 217 | 218 | struct SegmentedViewWithControl_Previews: PreviewProvider { 219 | static var previews: some View { 220 | SegmentedViewWithControl(title: "ABBasicStyle", styleType: .ASBasicStyle, axisMode: .horizontal, constant: .constant(ASConstant.init()), tabs: { 221 | Group { 222 | Image(systemName: "align.horizontal.left") 223 | .itemTag(0, selectArea: 0) { 224 | SelectionItemView("align.horizontal.left.fill") 225 | } 226 | Image(systemName: "align.horizontal.right") 227 | .itemTag(1, selectArea: 0) { 228 | SelectionItemView("align.horizontal.right.fill") 229 | } 230 | Image(systemName: "align.vertical.top") 231 | .itemTag(2, selectArea: 0) { 232 | SelectionItemView("align.vertical.top.fill") 233 | } 234 | Image(systemName: "align.vertical.bottom") 235 | .itemTag(3, selectArea: 0) { 236 | SelectionItemView("align.vertical.bottom.fill") 237 | } 238 | } 239 | }, style: { 240 | ASBasicStyle() 241 | }) 242 | .padding() 243 | .preferredColorScheme(.dark) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/SelectionItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectionItemView.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | struct SelectionItemView: View { 13 | 14 | @EnvironmentObject private var stateValue: ASStateValue 15 | @State private var scale: CGFloat = 1 16 | 17 | let iconName: String 18 | 19 | init(_ iconName: String) { 20 | self.iconName = iconName 21 | } 22 | 23 | var body: some View { 24 | Image(systemName: iconName) 25 | .foregroundColor(Color.white) 26 | .scaleEffect(scale) 27 | .onAppear { 28 | scale = 1 29 | if !stateValue.isInitialRun { 30 | withAnimation(.easeInOut(duration: 0.26)) { 31 | scale = 1.2 32 | } 33 | withAnimation(.easeInOut(duration: 0.26).delay(0.26)) { 34 | scale = 1 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/ViewModel/BacisValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BacisValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class BasicValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color.white.opacity(0.2), scale: 0.3)) 15 | 16 | @Published var backgroundColor: Color = .gray.opacity(0.3) 17 | @Published var foregroundColor: Color = .black.opacity(0.7) 18 | @Published var cornerRadius: CGFloat = 6 19 | @Published var padding: CGFloat = 3 20 | @Published var isApplySelectionCornerRadius: Bool = true 21 | @Published var movementMode: ASMovementMode = .viscosity 22 | 23 | @Published var selectArea0: CGFloat = 0 24 | @Published var selectArea1: CGFloat = 0 25 | @Published var selectArea2: CGFloat = 0 26 | } 27 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/ViewModel/CapsuleValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CapsuleValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class CapsuleValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var backgroundColor: Color = .gray.opacity(0.4) 17 | @Published var foregroundColor: Color = Color.blue 18 | @Published var movementMode: ASMovementMode = .viscosity 19 | 20 | @Published var selectArea0: CGFloat = 0 21 | @Published var selectArea1: CGFloat = 0 22 | @Published var selectArea2: CGFloat = 0 23 | @Published var selectArea3: CGFloat = 0 24 | } 25 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/ViewModel/JellyValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JellyValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class JellyValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var backgroundColor: Color = .gray.opacity(0.1) 17 | @Published var foregroundColor: Color = .purple 18 | @Published var jellyRadius: CGFloat = 110 19 | @Published var jellyDepth: CGFloat = 0.9 20 | @Published var jellyEdge: ASEdgeMode = .bottomTrailing 21 | 22 | @Published var selectArea0: CGFloat = 0 23 | @Published var selectArea1: CGFloat = 0 24 | @Published var selectArea2: CGFloat = 0 25 | @Published var selectArea3: CGFloat = 0 26 | } 27 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/ViewModel/LineValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class LineValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x202020), scale: 0)) 15 | 16 | @Published var lineColor: Color = .blue 17 | @Published var lineSmallWidth: CGFloat = 2 18 | @Published var lineLargeScale: CGFloat = 1.0 19 | @Published var lineEdge: ASEdgeMode = .bottomTrailing 20 | @Published var movementMode: ASMovementMode = .viscosity 21 | 22 | @Published var selectArea0: CGFloat = 0 23 | @Published var selectArea1: CGFloat = 0 24 | @Published var selectArea2: CGFloat = 0 25 | @Published var selectArea3: CGFloat = 0 26 | } 27 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/ViewModel/NeumorphismValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeumorphismValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class NeumorphismValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var backgroundColor: Color = .clear 17 | @Published var foregroundColor: Color = .clear 18 | @Published var cornerRadius: CGFloat = 11 19 | @Published var padding: CGFloat = 12 20 | @Published var shadowRadius: CGFloat = 5 21 | @Published var shadowOpacity: CGFloat = 0.7 22 | @Published var isInner: Bool = false 23 | @Published var movementMode: ASMovementMode = .viscosity 24 | 25 | @Published var selectArea0: CGFloat = 0 26 | @Published var selectArea1: CGFloat = 0 27 | @Published var selectArea2: CGFloat = 0 28 | @Published var selectArea3: CGFloat = 0 29 | 30 | init(backgroundColor: Color = Color(hex: 0x31353A), 31 | foregroundColor: Color = Color(hex: 0x31353A), 32 | cornerRadius: CGFloat = 11, 33 | padding: CGFloat = 12, 34 | shadowRadius: CGFloat = 5, 35 | shadowOpacity: CGFloat = 0.7, 36 | isInner: Bool = false, 37 | movementMode: ASMovementMode = .viscosity) { 38 | 39 | self.backgroundColor = backgroundColor 40 | self.foregroundColor = foregroundColor 41 | self.cornerRadius = cornerRadius 42 | self.padding = padding 43 | self.shadowRadius = shadowRadius 44 | self.isInner = isInner 45 | self.movementMode = movementMode 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/ViewModel/NormalValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NormalValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/24. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class NormalValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var selectArea0: CGFloat = 0 17 | @Published var selectArea1: CGFloat = 0 18 | 19 | } 20 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/ViewModel/ScaleValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScaleValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class ScaleValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var backgroundColor: Color = .clear 17 | @Published var foregroundColor: Color = Color.blue 18 | @Published var cornerRadius: CGFloat = 11 19 | @Published var minimumScale: CGFloat = 0.1 20 | 21 | @Published var selectArea0: CGFloat = 0 22 | @Published var selectArea1: CGFloat = 0 23 | @Published var selectArea2: CGFloat = 0 24 | @Published var selectArea3: CGFloat = 0 25 | } 26 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/ViewModel/ViscosityValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViscosityValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/24. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class ViscosityValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var selectArea0: CGFloat = 0 17 | @Published var selectArea1: CGFloat = 0 18 | @Published var selectArea2: CGFloat = 0 19 | } 20 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample (tvOS)/WithoutStyleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WithoutStyleView.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | struct WithoutStyleView: View { 13 | 14 | @State private var selection: Int = 0 15 | @State private var constant = ASConstant(axisMode: .horizontal) 16 | 17 | var body: some View { 18 | AxisSegmentedView(selection: $selection, constant: constant) { 19 | TabViews() 20 | } 21 | } 22 | } 23 | 24 | struct TabViews: View { 25 | 26 | @State private var maxArea1: CGFloat = 600 27 | @EnvironmentObject private var stateValue: ASStateValue 28 | 29 | let colors = [Color(hex: 0x295A76), Color(hex: 0x7FACAA), Color(hex: 0xEBF4CC), Color(hex: 0xE79875), Color(hex: 0xBA523C), Color(hex: 0x295A76)] 30 | 31 | var listView: some View { 32 | List(0...100, id: \.self) { index in 33 | Button { 34 | print("click") 35 | } label: { 36 | Text("Index \(index)") 37 | } 38 | }.listStyle(.plain) 39 | } 40 | 41 | var body: some View { 42 | Group { 43 | Rectangle() 44 | .fill(colors[0]) 45 | .overlay( 46 | Text("0") 47 | ) 48 | .itemTag(0, selectArea: maxArea1) { 49 | Circle() 50 | .fill(.red) 51 | .overlay( 52 | Text("0") 53 | ) 54 | } 55 | Rectangle() 56 | .fill(colors[1]) 57 | .overlay( 58 | Text("1") 59 | ) 60 | .itemTag(1, selectArea: maxArea1) { 61 | listView 62 | } 63 | Rectangle() 64 | .fill(colors[2]) 65 | .overlay( 66 | Text("2") 67 | ) 68 | .itemTag(2, selectArea: maxArea1) { 69 | listView 70 | } 71 | Rectangle() 72 | .fill(colors[3]) 73 | .overlay( 74 | Text("3") 75 | ) 76 | .itemTag(3, selectArea: maxArea1) { 77 | listView 78 | } 79 | } 80 | } 81 | } 82 | 83 | struct WithoutStyleView_Previews: PreviewProvider { 84 | static var previews: some View { 85 | WithoutStyleView() 86 | .preferredColorScheme(.dark) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/AxisSegmentedViewExample.xcodeproj/xcshareddata/xcschemes/AxisSegmentedView.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | }, 93 | { 94 | "idiom" : "mac", 95 | "scale" : "1x", 96 | "size" : "16x16" 97 | }, 98 | { 99 | "idiom" : "mac", 100 | "scale" : "2x", 101 | "size" : "16x16" 102 | }, 103 | { 104 | "idiom" : "mac", 105 | "scale" : "1x", 106 | "size" : "32x32" 107 | }, 108 | { 109 | "idiom" : "mac", 110 | "scale" : "2x", 111 | "size" : "32x32" 112 | }, 113 | { 114 | "idiom" : "mac", 115 | "scale" : "1x", 116 | "size" : "128x128" 117 | }, 118 | { 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "128x128" 122 | }, 123 | { 124 | "idiom" : "mac", 125 | "scale" : "1x", 126 | "size" : "256x256" 127 | }, 128 | { 129 | "idiom" : "mac", 130 | "scale" : "2x", 131 | "size" : "256x256" 132 | }, 133 | { 134 | "idiom" : "mac", 135 | "scale" : "1x", 136 | "size" : "512x512" 137 | }, 138 | { 139 | "idiom" : "mac", 140 | "scale" : "2x", 141 | "size" : "512x512" 142 | } 143 | ], 144 | "info" : { 145 | "author" : "xcode", 146 | "version" : 1 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/Assets.xcassets/image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "olimpia-campean-IkhxVv5Mkn4-unsplash.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "original" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/Assets.xcassets/image.imageset/olimpia-campean-IkhxVv5Mkn4-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasudev/AxisSegmentedView/44e42f5766fe3fd1dda6b1a3f2047127f9e51b2e/AxisSegmentedViewExample/Shared/Assets.xcassets/image.imageset/olimpia-campean-IkhxVv5Mkn4-unsplash.jpg -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/AxisSegmentedViewExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AxisSegmentedViewExampleApp.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/18. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @main 12 | struct AxisSegmentedViewExampleApp: App { 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/Color+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+Extensions.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2021/12/08. 6 | // Copyright (c) 2021 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public extension Color { 12 | 13 | init(hex: UInt, alpha: Double = 1) { 14 | self.init( 15 | .sRGB, 16 | red: Double((hex >> 16) & 0xff) / 255, 17 | green: Double((hex >> 08) & 0xff) / 255, 18 | blue: Double((hex >> 00) & 0xff) / 255, 19 | opacity: alpha 20 | ) 21 | } 22 | 23 | init(hex: String) { 24 | let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 25 | var int: UInt64 = 0 26 | Scanner(string: hex).scanHexInt64(&int) 27 | let a, r, g, b: UInt64 28 | switch hex.count { 29 | case 3: // RGB (12-bit) 30 | (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) 31 | case 6: // RGB (24-bit) 32 | (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) 33 | case 8: // ARGB (32-bit) 34 | (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) 35 | default: 36 | (a, r, g, b) = (1, 1, 1, 0) 37 | } 38 | 39 | self.init( 40 | .sRGB, 41 | red: Double(r) / 255, 42 | green: Double(g) / 255, 43 | blue: Double(b) / 255, 44 | opacity: Double(a) / 255 45 | ) 46 | } 47 | 48 | static var random: Color { 49 | return Color( 50 | red: .random(in: 0...1), 51 | green: .random(in: 0...1), 52 | blue: .random(in: 0...1) 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/18. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | struct ContentView: View { 13 | 14 | @State private var tabViewSelection: Int = 0 15 | 16 | private var content: some View { 17 | TabView(selection: $tabViewSelection) { 18 | SegmentedListView(axisMode: .horizontal) 19 | .tag(0) 20 | .tabItem { 21 | Image(systemName: "rectangle.arrowtriangle.2.inward") 22 | Text("Horizontal") 23 | } 24 | 25 | SegmentedListView(axisMode: .vertical) 26 | .tag(1) 27 | .tabItem { 28 | Image(systemName: "rectangle.portrait.arrowtriangle.2.inward") 29 | Text("Vertical") 30 | } 31 | 32 | WithoutStyleView() 33 | .tag(2) 34 | .tabItem { 35 | Image(systemName: "cpu") 36 | Text("Without style") 37 | } 38 | 39 | CustomStyleView() 40 | .tag(3) 41 | .tabItem { 42 | Image(systemName: "skew") 43 | Text("Custom Style") 44 | } 45 | .padding() 46 | } 47 | .navigationTitle(Text("AxisSegmentedView")) 48 | } 49 | var body: some View { 50 | ZStack { 51 | #if os(iOS) 52 | NavigationView { 53 | content 54 | .navigationBarTitleDisplayMode(.inline) 55 | } 56 | .navigationViewStyle(.stack) 57 | #else 58 | content 59 | .padding() 60 | #endif 61 | } 62 | .preferredColorScheme(.dark) 63 | } 64 | } 65 | 66 | struct ContentView_Previews: PreviewProvider { 67 | static var previews: some View { 68 | ContentView().preferredColorScheme(.dark) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/CustomStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomStyle.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/25. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | public struct CustomStyle: View { 13 | 14 | @EnvironmentObject var stateValue: ASStateValue 15 | 16 | let color: Color 17 | public init(color: Color = .purple) { 18 | self.color = color 19 | } 20 | 21 | private var selectionView: some View { 22 | RoundedRectangle(cornerRadius: 5) 23 | .fill(Color.yellow) 24 | } 25 | 26 | public var body: some View { 27 | let selectionFrame = stateValue.selectionFrame 28 | ZStack(alignment: .topLeading) { 29 | Color.clear 30 | RoundedRectangle(cornerRadius: 5) 31 | .stroke() 32 | .fill(color) 33 | .frame(width: selectionFrame.width, height: selectionFrame.height) 34 | .offset(x: selectionFrame.origin.x, y: selectionFrame.origin.y) 35 | } 36 | .animation(.easeInOut, value: stateValue.selectionIndex) 37 | } 38 | } 39 | 40 | struct CustomStyle_Previews: PreviewProvider { 41 | static var previews: some View { 42 | SegmentedViewPreview(constant: .init()) { 43 | CustomStyle() 44 | .preferredColorScheme(.dark) 45 | } 46 | .frame(height: 44) 47 | .padding(.horizontal, 16) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/CustomStyleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomStyleView.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/26. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | struct CustomStyleView: View { 13 | 14 | @State private var selection: Int = 0 15 | 16 | var body: some View { 17 | HStack { 18 | AxisSegmentedView(selection: $selection, constant: .init(axisMode: .vertical)) { 19 | Image(systemName: "align.horizontal.left") 20 | .itemTag(0, selectArea: 0) { 21 | SelectionItemView("align.horizontal.left.fill") 22 | } 23 | Image(systemName: "align.horizontal.right") 24 | .itemTag(1, selectArea: 260) { 25 | SelectionItemView("align.horizontal.right.fill") 26 | } 27 | Image(systemName: "align.vertical.top") 28 | .itemTag(2, selectArea: 0) { 29 | SelectionItemView("align.vertical.top.fill") 30 | } 31 | Image(systemName: "align.vertical.bottom") 32 | .itemTag(3, selectArea: 260) { 33 | SelectionItemView("align.vertical.bottom.fill") 34 | } 35 | } style: { 36 | CustomStyle(color: .blue) 37 | } onTapReceive: { selectionTap in 38 | /// Imperative syntax 39 | print("---------------------") 40 | print("Selection : ", selectionTap) 41 | print("Already selected : ", self.selection == selectionTap) 42 | } 43 | .frame(width: 44) 44 | 45 | AxisSegmentedView(selection: $selection, constant: .init()) { 46 | Image(systemName: "align.horizontal.left") 47 | .itemTag(0, selectArea: 0) { 48 | SelectionItemView("align.horizontal.left.fill") 49 | } 50 | Image(systemName: "align.horizontal.right") 51 | .itemTag(1, selectArea: 160) { 52 | SelectionItemView("align.horizontal.right.fill") 53 | } 54 | Image(systemName: "align.vertical.top") 55 | .itemTag(2, selectArea: 0) { 56 | SelectionItemView("align.vertical.top.fill") 57 | } 58 | Image(systemName: "align.vertical.bottom") 59 | .itemTag(3, selectArea: 160) { 60 | SelectionItemView("align.vertical.bottom.fill") 61 | } 62 | } style: { 63 | CustomStyle(color: .red) 64 | } onTapReceive: { selectionTap in 65 | /// Imperative syntax 66 | print("---------------------") 67 | print("Selection : ", selectionTap) 68 | print("Already selected : ", self.selection == selectionTap) 69 | } 70 | .frame(height: 44) 71 | } 72 | } 73 | } 74 | 75 | struct CustomStyleView_Previews: PreviewProvider { 76 | static var previews: some View { 77 | CustomStyleView() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/SegmentedListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SegmentedListView.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | enum StyleType: String { 13 | case ASNormalStyle 14 | case ASViscosityStyle 15 | 16 | case ASBasicStyle 17 | case ASCapsuleStyle 18 | case ASJellyStyle 19 | case ASLineStyle 20 | case ASNeumorphismStyle 21 | case ASScaleStyle 22 | } 23 | 24 | 25 | struct SegmentedListView: View { 26 | 27 | let axisMode: ASAxisMode 28 | 29 | @StateObject private var normalValue: NormalValue = .init() 30 | @StateObject private var viscosityValue: ViscosityValue = .init() 31 | 32 | @StateObject private var basicValue: BasicValue = .init() 33 | @StateObject private var capsuleValue: CapsuleValue = .init() 34 | @StateObject private var jellyValue: JellyValue = .init() 35 | @StateObject private var lineValue: LineValue = .init() 36 | @StateObject private var neumorphismValue: NeumorphismValue = .init() 37 | @StateObject private var scaleValue: ScaleValue = .init() 38 | 39 | var content: some View { 40 | Group { 41 | SegmentedViewWithControl(title: "ABNormalStyle", styleType: .ASNormalStyle, axisMode: axisMode, constant: $normalValue.constant, tabs: { 42 | Group { 43 | Text("Clear") 44 | .font(.callout) 45 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 46 | .foregroundColor(Color.white.opacity(0.5)) 47 | .itemTag(0, selectArea: normalValue.selectArea0) { 48 | HStack { 49 | Image(systemName: "checkmark.circle") 50 | Text("Clear") 51 | } 52 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 53 | .font(.callout) 54 | .foregroundColor(Color.white) 55 | } 56 | Text("Confusing") 57 | .font(.callout) 58 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 59 | .foregroundColor(Color.white.opacity(0.5)) 60 | .itemTag(1, selectArea: normalValue.selectArea1) { 61 | HStack { 62 | Image(systemName: "checkmark.circle") 63 | Text("Confusing") 64 | } 65 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 66 | .font(.callout) 67 | .foregroundColor(Color.white) 68 | } 69 | } 70 | }, style: { 71 | ASNormalStyle { _ in 72 | RoundedRectangle(cornerRadius: 5) 73 | .fill(Color(hex: 0x191919)) 74 | .overlay( 75 | RoundedRectangle(cornerRadius: 5) 76 | .stroke() 77 | .fill(Color(hex: 0x282828)) 78 | ) 79 | .padding(3.5) 80 | } 81 | .background(Color(hex: 0x0B0C10)) 82 | .clipShape(RoundedRectangle(cornerRadius: 5)) 83 | }) 84 | SegmentedViewWithControl(title: "ASViscosityStyle", styleType: .ASViscosityStyle, axisMode: axisMode, constant: $viscosityValue.constant, tabs: { 85 | Group { 86 | Text("Store") 87 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 88 | .font(.callout) 89 | .foregroundColor(Color.white.opacity(0.5)) 90 | .itemTag(0, selectArea: viscosityValue.selectArea0) { 91 | Text("Store") 92 | .font(.callout) 93 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 94 | .foregroundColor(Color.white) 95 | } 96 | Text("Library") 97 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 98 | .font(.callout) 99 | .foregroundColor(Color.white.opacity(0.5)) 100 | .itemTag(1, selectArea: viscosityValue.selectArea1) { 101 | Text("Library") 102 | .font(.callout) 103 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 104 | .foregroundColor(Color.white) 105 | } 106 | Text("Downloads") 107 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 108 | .font(.callout) 109 | .foregroundColor(Color.white.opacity(0.5)) 110 | .itemTag(2, selectArea: viscosityValue.selectArea2) { 111 | Text("Downloads") 112 | .font(.callout) 113 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 114 | .foregroundColor(Color.white) 115 | } 116 | } 117 | }, style: { 118 | ASViscosityStyle { _ in 119 | Capsule() 120 | .fill(LinearGradient(colors: [Color(hex: 0x222222), Color(hex: 0x111111)], 121 | startPoint: axisMode == .horizontal ? UnitPoint.top : UnitPoint.leading, 122 | endPoint: axisMode == .horizontal ? UnitPoint.bottom : UnitPoint.trailing)) 123 | .overlay( 124 | Capsule() 125 | .stroke() 126 | .fill(Color.black) 127 | ) 128 | .padding(2) 129 | } 130 | .background(Color.black.opacity(0.2)) 131 | .clipShape(Capsule()) 132 | .innerShadow(Capsule(), radius: 1, opacity: 0.5, isDark: true) 133 | }) 134 | 135 | SegmentedViewWithControl(title: "ASBasicStyle", styleType: .ASBasicStyle, axisMode: axisMode, constant: $basicValue.constant, tabs: { 136 | Group { 137 | Text("Male") 138 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 139 | .font(.callout) 140 | .foregroundColor(Color.white.opacity(0.5)) 141 | .itemTag(0, selectArea: basicValue.selectArea0) { 142 | Text("Male") 143 | .font(.callout) 144 | .fixedSize() 145 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 146 | .foregroundColor(Color.white) 147 | } 148 | Text("Female") 149 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 150 | .font(.callout) 151 | .foregroundColor(Color.white.opacity(0.5)) 152 | .itemTag(1, selectArea: basicValue.selectArea1) { 153 | Text("Female") 154 | .font(.callout) 155 | .fixedSize() 156 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 157 | .foregroundColor(Color.white) 158 | } 159 | Text("Other") 160 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 161 | .font(.callout) 162 | .foregroundColor(Color.white.opacity(0.5)) 163 | .itemTag(2, selectArea: basicValue.selectArea2) { 164 | Text("Other") 165 | .font(.callout) 166 | .fixedSize() 167 | .rotationEffect(Angle(degrees: axisMode == .horizontal ? 0 : -90)) 168 | .foregroundColor(Color.white) 169 | } 170 | } 171 | }, style: { 172 | ASBasicStyle(backgroundColor: basicValue.backgroundColor, 173 | foregroundColor: basicValue.foregroundColor, 174 | cornerRadius: basicValue.cornerRadius, 175 | padding: basicValue.padding, 176 | isApplySelectionCornerRadius: basicValue.isApplySelectionCornerRadius, 177 | movementMode: basicValue.movementMode) 178 | }) 179 | 180 | SegmentedViewWithControl(title: "ASJellyStyle", styleType: .ASJellyStyle, axisMode: axisMode, constant: $jellyValue.constant, tabs: { 181 | Group { 182 | Image(systemName: "align.horizontal.left") 183 | .itemTag(0, selectArea: jellyValue.selectArea0) { 184 | SelectionItemView("align.horizontal.left.fill") 185 | } 186 | Image(systemName: "align.horizontal.right") 187 | .itemTag(1, selectArea: jellyValue.selectArea1) { 188 | SelectionItemView("align.horizontal.right.fill") 189 | } 190 | Image(systemName: "align.vertical.top") 191 | .itemTag(2, selectArea: jellyValue.selectArea2) { 192 | SelectionItemView("align.vertical.top.fill") 193 | } 194 | Image(systemName: "align.vertical.bottom") 195 | .itemTag(3, selectArea: jellyValue.selectArea3) { 196 | SelectionItemView("align.vertical.bottom.fill") 197 | } 198 | } 199 | }, style: { 200 | ASJellyStyle(backgroundColor: jellyValue.backgroundColor, 201 | foregroundColor: jellyValue.foregroundColor, 202 | jellyRadius: jellyValue.jellyRadius, 203 | jellyDepth: jellyValue.jellyDepth, 204 | jellyEdge: jellyValue.jellyEdge) 205 | // .overlay( 206 | // RoundedRectangle(cornerRadius: 11) 207 | // .stroke(.purple, lineWidth: 1) 208 | // .padding(1) 209 | // ) 210 | // .clipShape(RoundedRectangle(cornerRadius: 11)) 211 | }) 212 | 213 | SegmentedViewWithControl(title: "ASLineStyle", styleType: .ASLineStyle, axisMode: axisMode, constant: $lineValue.constant, tabs: { 214 | Group { 215 | Image(systemName: "align.horizontal.left") 216 | .itemTag(0, selectArea: lineValue.selectArea0) { 217 | SelectionItemView("align.horizontal.left.fill") 218 | } 219 | Image(systemName: "align.horizontal.right") 220 | .itemTag(1, selectArea: lineValue.selectArea1) { 221 | SelectionItemView("align.horizontal.right.fill") 222 | } 223 | Image(systemName: "align.vertical.top") 224 | .itemTag(2, selectArea: lineValue.selectArea2) { 225 | SelectionItemView("align.vertical.top.fill") 226 | } 227 | Image(systemName: "align.vertical.bottom") 228 | .itemTag(3, selectArea: lineValue.selectArea3) { 229 | SelectionItemView("align.vertical.bottom.fill") 230 | } 231 | } 232 | }, style: { 233 | ASLineStyle(lineColor: lineValue.lineColor, 234 | lineSmallWidth: lineValue.lineSmallWidth, 235 | lineLargeScale: lineValue.lineLargeScale, 236 | lineEdge: lineValue.lineEdge, 237 | movementMode: lineValue.movementMode) 238 | .overlay( 239 | Rectangle() 240 | .stroke() 241 | .fill(Color(hex: 0x303030)) 242 | ) 243 | }) 244 | 245 | SegmentedViewWithControl(title: "ASCapsuleStyle", styleType: .ASCapsuleStyle, axisMode: axisMode, constant: $capsuleValue.constant, tabs: { 246 | Group { 247 | Image(systemName: "align.horizontal.left") 248 | .itemTag(0, selectArea: capsuleValue.selectArea0) { 249 | SelectionItemView("align.horizontal.left.fill") 250 | } 251 | Image(systemName: "align.horizontal.right") 252 | .itemTag(1, selectArea: capsuleValue.selectArea1) { 253 | SelectionItemView("align.horizontal.right.fill") 254 | } 255 | Image(systemName: "align.vertical.top") 256 | .itemTag(2, selectArea: capsuleValue.selectArea2) { 257 | SelectionItemView("align.vertical.top.fill") 258 | } 259 | Image(systemName: "align.vertical.bottom") 260 | .itemTag(3, selectArea: capsuleValue.selectArea3) { 261 | SelectionItemView("align.vertical.bottom.fill") 262 | } 263 | } 264 | }, style: { 265 | ASCapsuleStyle(backgroundColor: capsuleValue.backgroundColor, 266 | foregroundColor: capsuleValue.foregroundColor, 267 | movementMode: capsuleValue.movementMode) 268 | }) 269 | 270 | SegmentedViewWithControl(title: "ASNeumorphismStyle", styleType: .ASNeumorphismStyle, axisMode: axisMode, constant: $neumorphismValue.constant, area: 70, tabs: { 271 | Group { 272 | Image(systemName: "align.horizontal.left") 273 | .itemTag(0, selectArea: neumorphismValue.selectArea0) { 274 | SelectionItemView("align.horizontal.left.fill") 275 | } 276 | Image(systemName: "align.horizontal.right") 277 | .itemTag(1, selectArea: neumorphismValue.selectArea1) { 278 | SelectionItemView("align.horizontal.right.fill") 279 | } 280 | Image(systemName: "align.vertical.top") 281 | .itemTag(2, selectArea: neumorphismValue.selectArea2) { 282 | SelectionItemView("align.vertical.top.fill") 283 | } 284 | Image(systemName: "align.vertical.bottom") 285 | .itemTag(3, selectArea: neumorphismValue.selectArea3) { 286 | SelectionItemView("align.vertical.bottom.fill") 287 | } 288 | } 289 | }, style: { 290 | ASNeumorphismStyle(backgroundColor: neumorphismValue.backgroundColor, 291 | foregroundColor: neumorphismValue.foregroundColor, 292 | cornerRadius: neumorphismValue.cornerRadius, 293 | padding: neumorphismValue.padding, 294 | shadowRadius: neumorphismValue.shadowRadius, 295 | shadowOpacity: neumorphismValue.shadowOpacity, 296 | isInner: neumorphismValue.isInner, 297 | movementMode: neumorphismValue.movementMode) 298 | }) 299 | 300 | SegmentedViewWithControl(title: "ASScaleStyle", styleType: .ASScaleStyle, axisMode: axisMode, constant: $scaleValue.constant, tabs: { 301 | Group { 302 | Image(systemName: "align.horizontal.left") 303 | .itemTag(0, selectArea: scaleValue.selectArea0) { 304 | SelectionItemView("align.horizontal.left.fill") 305 | } 306 | Image(systemName: "align.horizontal.right") 307 | .itemTag(1, selectArea: scaleValue.selectArea1) { 308 | SelectionItemView("align.horizontal.right.fill") 309 | } 310 | Image(systemName: "align.vertical.top") 311 | .itemTag(2, selectArea: scaleValue.selectArea2) { 312 | SelectionItemView("align.vertical.top.fill") 313 | } 314 | Image(systemName: "align.vertical.bottom") 315 | .itemTag(3, selectArea: scaleValue.selectArea3) { 316 | SelectionItemView("align.vertical.bottom.fill") 317 | } 318 | } 319 | }, style: { 320 | ASScaleStyle(backgroundColor: scaleValue.backgroundColor, 321 | foregroundColor: scaleValue.foregroundColor, 322 | cornerRadius: scaleValue.cornerRadius, 323 | minimumScale: scaleValue.minimumScale) 324 | }) 325 | } 326 | } 327 | var body: some View { 328 | ZStack { 329 | if axisMode == .horizontal { 330 | ScrollView { 331 | VStack(spacing: 20) { 332 | content 333 | } 334 | .padding(.horizontal, 5) 335 | } 336 | }else { 337 | ScrollView(.horizontal) { 338 | HStack(spacing: 20) { 339 | content 340 | } 341 | .padding(.vertical, 5) 342 | } 343 | } 344 | } 345 | .environmentObject(normalValue) 346 | .environmentObject(viscosityValue) 347 | .environmentObject(basicValue) 348 | .environmentObject(capsuleValue) 349 | .environmentObject(jellyValue) 350 | .environmentObject(lineValue) 351 | .environmentObject(neumorphismValue) 352 | .environmentObject(scaleValue) 353 | } 354 | } 355 | 356 | struct SegmentedListView_Previews: PreviewProvider { 357 | static var previews: some View { 358 | SegmentedListView(axisMode: .horizontal) 359 | .padding() 360 | .preferredColorScheme(.dark) 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/SelectionItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectionItemView.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | struct SelectionItemView: View { 13 | 14 | @EnvironmentObject private var stateValue: ASStateValue 15 | @State private var scale: CGFloat = 1 16 | 17 | let iconName: String 18 | 19 | init(_ iconName: String) { 20 | self.iconName = iconName 21 | } 22 | 23 | var body: some View { 24 | Image(systemName: iconName) 25 | .foregroundColor(Color.white) 26 | .scaleEffect(scale) 27 | .onAppear { 28 | scale = 1 29 | if !stateValue.isInitialRun { 30 | withAnimation(.easeInOut(duration: 0.26)) { 31 | scale = 1.2 32 | } 33 | withAnimation(.easeInOut(duration: 0.26).delay(0.26)) { 34 | scale = 1 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/ViewModel/BacisValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BacisValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class BasicValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color.white.opacity(0.2), scale: 0.3)) 15 | 16 | @Published var backgroundColor: Color = .gray.opacity(0.3) 17 | @Published var foregroundColor: Color = .black.opacity(0.7) 18 | @Published var cornerRadius: CGFloat = 6 19 | @Published var padding: CGFloat = 3 20 | @Published var isApplySelectionCornerRadius: Bool = true 21 | @Published var movementMode: ASMovementMode = .viscosity 22 | 23 | @Published var selectArea0: CGFloat = 0 24 | @Published var selectArea1: CGFloat = 0 25 | @Published var selectArea2: CGFloat = 0 26 | } 27 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/ViewModel/CapsuleValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CapsuleValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class CapsuleValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var backgroundColor: Color = .gray.opacity(0.4) 17 | @Published var foregroundColor: Color = Color.blue 18 | @Published var movementMode: ASMovementMode = .viscosity 19 | 20 | @Published var selectArea0: CGFloat = 0 21 | @Published var selectArea1: CGFloat = 0 22 | @Published var selectArea2: CGFloat = 0 23 | @Published var selectArea3: CGFloat = 0 24 | } 25 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/ViewModel/JellyValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JellyValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class JellyValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var backgroundColor: Color = .gray.opacity(0.1) 17 | @Published var foregroundColor: Color = .purple 18 | @Published var jellyRadius: CGFloat = 56 19 | @Published var jellyDepth: CGFloat = 0.9 20 | @Published var jellyEdge: ASEdgeMode = .bottomTrailing 21 | 22 | @Published var selectArea0: CGFloat = 0 23 | @Published var selectArea1: CGFloat = 0 24 | @Published var selectArea2: CGFloat = 0 25 | @Published var selectArea3: CGFloat = 0 26 | } 27 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/ViewModel/LineValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class LineValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x202020), scale: 0)) 15 | 16 | @Published var lineColor: Color = .blue 17 | @Published var lineSmallWidth: CGFloat = 2 18 | @Published var lineLargeScale: CGFloat = 1.0 19 | @Published var lineEdge: ASEdgeMode = .bottomTrailing 20 | @Published var movementMode: ASMovementMode = .viscosity 21 | 22 | @Published var selectArea0: CGFloat = 0 23 | @Published var selectArea1: CGFloat = 0 24 | @Published var selectArea2: CGFloat = 0 25 | @Published var selectArea3: CGFloat = 0 26 | } 27 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/ViewModel/NeumorphismValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeumorphismValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class NeumorphismValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var backgroundColor: Color = .clear 17 | @Published var foregroundColor: Color = .clear 18 | @Published var cornerRadius: CGFloat = 11 19 | @Published var padding: CGFloat = 12 20 | @Published var shadowRadius: CGFloat = 5 21 | @Published var shadowOpacity: CGFloat = 0.7 22 | @Published var isInner: Bool = false 23 | @Published var movementMode: ASMovementMode = .viscosity 24 | 25 | @Published var selectArea0: CGFloat = 0 26 | @Published var selectArea1: CGFloat = 0 27 | @Published var selectArea2: CGFloat = 0 28 | @Published var selectArea3: CGFloat = 0 29 | 30 | init(backgroundColor: Color = Color(hex: 0x31353A), 31 | foregroundColor: Color = Color(hex: 0x31353A), 32 | cornerRadius: CGFloat = 11, 33 | padding: CGFloat = 12, 34 | shadowRadius: CGFloat = 5, 35 | shadowOpacity: CGFloat = 0.7, 36 | isInner: Bool = false, 37 | movementMode: ASMovementMode = .viscosity) { 38 | 39 | self.backgroundColor = backgroundColor 40 | self.foregroundColor = foregroundColor 41 | self.cornerRadius = cornerRadius 42 | self.padding = padding 43 | self.shadowRadius = shadowRadius 44 | self.isInner = isInner 45 | self.movementMode = movementMode 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/ViewModel/NormalValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NormalValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/24. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class NormalValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var selectArea0: CGFloat = 0 17 | @Published var selectArea1: CGFloat = 0 18 | 19 | } 20 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/ViewModel/ScaleValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScaleValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class ScaleValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var backgroundColor: Color = .clear 17 | @Published var foregroundColor: Color = Color.blue 18 | @Published var cornerRadius: CGFloat = 11 19 | @Published var minimumScale: CGFloat = 0.1 20 | 21 | @Published var selectArea0: CGFloat = 0 22 | @Published var selectArea1: CGFloat = 0 23 | @Published var selectArea2: CGFloat = 0 24 | @Published var selectArea3: CGFloat = 0 25 | } 26 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/ViewModel/ViscosityValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViscosityValue.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/24. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | class ViscosityValue: ObservableObject { 13 | 14 | @Published var constant = ASConstant(divideLine: .init(color: Color(hex: 0x444444), scale: 0)) 15 | 16 | @Published var selectArea0: CGFloat = 0 17 | @Published var selectArea1: CGFloat = 0 18 | @Published var selectArea2: CGFloat = 0 19 | } 20 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/Shared/WithoutStyleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WithoutStyleView.swift 3 | // AxisSegmentedViewExample 4 | // 5 | // Created by jasu on 2022/03/23. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AxisSegmentedView 11 | 12 | struct WithoutStyleView: View { 13 | 14 | @State private var selection1: Int = 0 15 | @State private var constant1 = ASConstant(axisMode: .vertical) 16 | 17 | @State private var selection2: Int = 0 18 | @State private var constant2 = ASConstant(axisMode: .horizontal) 19 | 20 | var body: some View { 21 | VStack { 22 | AxisSegmentedView(selection: $selection1, constant: constant1) { 23 | TabViews() 24 | } onTapReceive: { selectionTap in 25 | /// Imperative syntax 26 | print("---------------------") 27 | print("Selection : ", selectionTap) 28 | print("Already selected : ", self.selection1 == selectionTap) 29 | } 30 | .clipped() 31 | AxisSegmentedView(selection: $selection2, constant: constant2) { 32 | TabViews() 33 | } onTapReceive: { selectionTap in 34 | /// Imperative syntax 35 | print("---------------------") 36 | print("Selection : ", selectionTap) 37 | print("Already selected : ", self.selection2 == selectionTap) 38 | } 39 | .clipped() 40 | } 41 | } 42 | } 43 | 44 | struct TabViews: View { 45 | 46 | @State private var maxArea1: CGFloat = 200 47 | @EnvironmentObject private var stateValue: ASStateValue 48 | 49 | let colors = [Color(hex: 0x295A76), Color(hex: 0x7FACAA), Color(hex: 0xEBF4CC), Color(hex: 0xE79875), Color(hex: 0xBA523C), Color(hex: 0x295A76)] 50 | 51 | var listView: some View { 52 | List(0...100, id: \.self) { index in 53 | Button { 54 | print("click") 55 | } label: { 56 | Text("Index \(index)") 57 | } 58 | }.listStyle(.plain) 59 | } 60 | 61 | var body: some View { 62 | Group { 63 | Rectangle() 64 | .fill(colors[0]) 65 | .overlay( 66 | Text("0") 67 | ) 68 | .itemTag(0, selectArea: maxArea1) { 69 | Rectangle() 70 | .fill(.red) 71 | .overlay( 72 | Text("0") 73 | ) 74 | } 75 | Rectangle() 76 | .fill(colors[1]) 77 | .overlay( 78 | Text("1") 79 | ) 80 | .itemTag(1, selectArea: maxArea1) { 81 | listView 82 | } 83 | Rectangle() 84 | .fill(colors[2]) 85 | .overlay( 86 | Text("2") 87 | ) 88 | .itemTag(2, selectArea: maxArea1) { 89 | listView 90 | } 91 | Rectangle() 92 | .fill(colors[3]) 93 | .overlay( 94 | Text("3") 95 | ) 96 | .itemTag(3, selectArea: maxArea1) { 97 | listView 98 | } 99 | } 100 | } 101 | } 102 | 103 | struct WithoutStyleView_Previews: PreviewProvider { 104 | static var previews: some View { 105 | WithoutStyleView() 106 | .preferredColorScheme(.dark) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /AxisSegmentedViewExample/macOS/macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 jasu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Markdown/AxisSegmentedView1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasudev/AxisSegmentedView/44e42f5766fe3fd1dda6b1a3f2047127f9e51b2e/Markdown/AxisSegmentedView1.png -------------------------------------------------------------------------------- /Markdown/AxisSegmentedView2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasudev/AxisSegmentedView/44e42f5766fe3fd1dda6b1a3f2047127f9e51b2e/Markdown/AxisSegmentedView2.png -------------------------------------------------------------------------------- /Markdown/AxisSegmentedView3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasudev/AxisSegmentedView/44e42f5766fe3fd1dda6b1a3f2047127f9e51b2e/Markdown/AxisSegmentedView3.png -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.6 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "AxisSegmentedView", 8 | platforms: [ 9 | .iOS(.v14), 10 | .tvOS(.v14), 11 | .macOS(.v11) 12 | ], 13 | products: [ 14 | // Products define the executables and libraries a package produces, and make them visible to other packages. 15 | .library( 16 | name: "AxisSegmentedView", 17 | targets: ["AxisSegmentedView"]), 18 | ], 19 | dependencies: [ 20 | // Dependencies declare other packages that this package depends on. 21 | // .package(url: /* package url */, from: "1.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 26 | .target( 27 | name: "AxisSegmentedView", 28 | dependencies: []), 29 | .testTarget( 30 | name: "AxisSegmentedViewTests", 31 | dependencies: ["AxisSegmentedView"]), 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **AxisSegmentedView for SwiftUI** 2 | A library that allows you to easily create various styles of segmented views. Supports iOS, macOS and tvOS. 3 | 4 | [![Platforms](https://img.shields.io/badge/Platforms-iOS%20%7C%20macOS-blue?style=flat-square)](https://developer.apple.com/macOS) 5 | [![iOS](https://img.shields.io/badge/iOS-14.0-blue.svg)](https://developer.apple.com/iOS) 6 | [![macOS](https://img.shields.io/badge/macOS-11.0-blue.svg)](https://developer.apple.com/macOS) 7 | [![tvOS](https://img.shields.io/badge/tvOS-14.0-blue.svg)](https://developer.apple.com/tvOS) 8 | [![instagram](https://img.shields.io/badge/instagram-@dev.fabula-orange.svg?style=flat-square)](https://www.instagram.com/dev.fabula) 9 | [![SPM](https://img.shields.io/badge/SPM-compatible-red?style=flat-square)](https://developer.apple.com/documentation/swift_packages/package/) 10 | [![MIT](https://img.shields.io/badge/licenses-MIT-red.svg)](https://opensource.org/licenses/MIT) 11 | 12 | ## Screenshot 13 | |Horizontal|Vertical|For use without style| 14 | |:---:|:---:|:---:| 15 | |||| 16 | 17 | 18 | https://user-images.githubusercontent.com/1617304/160249891-a2fe15f2-5b07-4c2c-a204-fa9bd8981989.mov 19 | 20 | 21 | ## Example 22 | [https://fabulaapp.page.link/234](https://fabulaapp.page.link/234) 23 | 24 | ## Usages 25 | ```swift 26 | AxisSegmentedView(selection: $selection, constant: .init()) { 27 | Image(systemName: "align.horizontal.left") 28 | .itemTag(0, selectArea: 0) { 29 | Image(systemName: "align.horizontal.left.fill") 30 | } 31 | Image(systemName: "align.horizontal.right") 32 | .itemTag(1, selectArea: 160) { 33 | Image(systemName: "align.horizontal.right.fill") 34 | } 35 | Image(systemName: "align.vertical.top") 36 | .itemTag(2, selectArea: 0) { 37 | Image(systemName: "align.vertical.top.fill") 38 | } 39 | Image(systemName: "align.vertical.bottom") 40 | .itemTag(3, selectArea: 160) { 41 | Image(systemName: "align.vertical.bottom.fill") 42 | } 43 | } style: { 44 | ASBasicStyle() 45 | } onTapReceive: { selectionTap in 46 | /// Imperative syntax 47 | print("---------------------") 48 | print("Selection : ", selectionTap) 49 | print("Already selected : ", self.selection == selectionTap) 50 | } 51 | .frame(height: 44) 52 | ``` 53 | 54 | ## Usages - For use without style 55 | ```swift 56 | var listView: some View { 57 | List(0...100, id: \.self) { index in 58 | Button { 59 | print("click") 60 | } label: { 61 | Text("Index \(index)") 62 | } 63 | }.listStyle(.plain) 64 | } 65 | 66 | AxisSegmentedView(selection: $selection, constant: .init()) { 67 | Rectangle() 68 | .overlay( 69 | Text("0") 70 | ) 71 | .itemTag(0, selectArea: maxArea1) { 72 | Rectangle() 73 | .overlay( 74 | Text("0") 75 | ) 76 | } 77 | Rectangle() 78 | .overlay( 79 | Text("1") 80 | ) 81 | .itemTag(1, selectArea: maxArea1) { 82 | listView 83 | } 84 | Rectangle() 85 | .overlay( 86 | Text("2") 87 | ) 88 | .itemTag(2, selectArea: maxArea1) { 89 | listView 90 | } 91 | Rectangle() 92 | .overlay( 93 | Text("3") 94 | ) 95 | .itemTag(3, selectArea: maxArea1) { 96 | listView 97 | } 98 | } 99 | ``` 100 | 101 | ## Swift Package Manager 102 | The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler. Once you have your Swift package set up, adding AxisSegmentedView as a dependency is as easy as adding it to the dependencies value of your Package.swift. 103 | 104 | ```swift 105 | dependencies: [ 106 | .package(url: "https://github.com/jasudev/AxisSegmentedView.git", .branch("main")) 107 | ] 108 | ``` 109 | 110 | ## Contact 111 | instagram : [@dev.fabula](https://www.instagram.com/dev.fabula) 112 | email : [dev.fabula@gmail.com](mailto:dev.fabula@gmail.com) 113 | 114 | ## License 115 | AxisSegmentedView is available under the MIT license. See the [LICENSE](LICENSE) file for more info. 116 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/AxisSegmentedView.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // AxisSegmentedView.swift 4 | // AxisSegmentedView 5 | // 6 | // Created by jasu on 2022/03/19. 7 | // Copyright (c) 2022 jasu All rights reserved. 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is furnished 14 | // to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in all 17 | // copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 20 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 21 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 23 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 24 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import SwiftUI 28 | 29 | public struct AxisSegmentedView : View where SelectionValue : Hashable, Content : View, Style : View { 30 | 31 | @StateObject private var stateValue: ASStateValue = .init() 32 | @StateObject private var positionValue: ASPositionValue = .init() 33 | @State private var currentSize: CGSize = .zero 34 | 35 | private let selectionValue: ASSelectionValue 36 | private let constant: ASConstant 37 | 38 | public var content: () -> Content 39 | public var style: (() -> Style)? = nil 40 | 41 | public var body: some View { 42 | GeometryReader { proxy in 43 | ZStack { 44 | if currentSize != proxy.size { 45 | if positionValue.toggleSelectArea { 46 | getContent(proxy) 47 | }else { 48 | getContent(proxy) 49 | } 50 | }else { 51 | if positionValue.toggleSelectArea { 52 | getContent(proxy) 53 | }else { 54 | getContent(proxy) 55 | } 56 | } 57 | } 58 | } 59 | .animation(constant.animation ?? .none, value: selectionValue.selection) 60 | .environmentObject(selectionValue) 61 | .environmentObject(positionValue) 62 | .environmentObject(stateValue) 63 | .onChange(of: constant) { newValue in 64 | stateValue.constant = newValue 65 | positionValue.constant = newValue 66 | } 67 | } 68 | 69 | private func getContent(_ proxy: GeometryProxy) -> some View { 70 | ZStack { 71 | Color.clear 72 | if constant.axisMode == .horizontal { 73 | HStack(spacing: 0) { 74 | content() 75 | } 76 | }else { 77 | VStack(spacing: 0) { 78 | content() 79 | } 80 | } 81 | } 82 | .onAppear { 83 | DispatchQueue.main.async { 84 | positionValue.isHasStyle = self.style != nil 85 | currentSize = proxy.size 86 | } 87 | } 88 | .backgroundPreferenceValue(ASItemPreferenceKey.self) { items in 89 | getBackground(items, size: proxy.size) 90 | } 91 | } 92 | 93 | private func getBackground(_ items: [ASItem], size: CGSize) -> some View { 94 | return Color.clear 95 | .overlay(style?()) 96 | .onAppear { 97 | positionValue.constant = constant 98 | positionValue.items = items 99 | positionValue.size = size 100 | } 101 | } 102 | } 103 | 104 | public extension AxisSegmentedView where SelectionValue: Hashable, Content: View, Style: View { 105 | 106 | /// Initializes `AxisSegmentedView` 107 | /// - Parameters: 108 | /// - selection: The currently selected tap value. 109 | /// - constant: A constant value for segmented view. 110 | /// - content: Content views with tab items applied. 111 | /// - style: The style of the segmented view. 112 | /// - onTapReceive: Method that treats the currently selected tab as imperative syntax. 113 | init(selection: Binding, 114 | constant: ASConstant = .init(), 115 | @ViewBuilder _ content: @escaping () -> Content, 116 | @ViewBuilder style: @escaping () -> Style, 117 | onTapReceive: ((SelectionValue) -> Void)? = nil) { 118 | 119 | self.selectionValue = ASSelectionValue(selection: selection, onTapReceive: onTapReceive) 120 | self.constant = constant 121 | self.style = style 122 | self.content = content 123 | } 124 | } 125 | 126 | public extension AxisSegmentedView where SelectionValue: Hashable, Content: View, Style == EmptyView { 127 | 128 | /// Initializes `AxisSegmentedView` 129 | /// - Parameters: 130 | /// - selection: The currently selected tap value. 131 | /// - constant: A constant value for segmented view. 132 | /// - content: Content views with tab items applied. 133 | /// - onTapReceive: Method that treats the currently selected tab as imperative syntax. 134 | init(selection: Binding, 135 | constant: ASConstant = .init(), 136 | @ViewBuilder _ content: @escaping () -> Content, 137 | onTapReceive: ((SelectionValue) -> Void)? = nil) { 138 | 139 | self.selectionValue = ASSelectionValue(selection: selection, onTapReceive: onTapReceive) 140 | self.constant = constant 141 | self.content = content 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Private/Model/ASItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASItem.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/18. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Information on the tab button. 29 | struct ASItem: Identifiable { 30 | 31 | let id = UUID() 32 | 33 | /// A tag that separates the tab view. 34 | let tag: Any 35 | 36 | /// The size of the selected tab. 37 | /// If it is less than or equal to 0, it is the same as normal size. 38 | let selectArea: CGFloat 39 | 40 | /// The tab button view in the selected state. 41 | let select: AnyView 42 | 43 | /// Initializes `ASItem` 44 | /// - Parameters: 45 | /// - tag: A tag that separates the tab view. 46 | /// - selectArea: The size of the selected tab. If it is less than or equal to 0, it is the same as normal size. 47 | /// - select: The tab button view in the selected state. 48 | init(tag: Any, selectArea: CGFloat = 0, select: V) { 49 | self.tag = tag 50 | self.selectArea = selectArea 51 | self.select = AnyView(select) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Private/Modifiers/ASItemModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASItemModifier.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/18. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | struct ASItemPreferenceKey: PreferenceKey { 29 | typealias Value = [ASItem] 30 | static var defaultValue: [ASItem] = [] 31 | static func reduce(value: inout [ASItem], nextValue: () -> [ASItem]) { 32 | value.append(contentsOf: nextValue()) 33 | } 34 | } 35 | 36 | struct ASItemModifier: ViewModifier { 37 | 38 | @EnvironmentObject private var stateValue: ASStateValue 39 | @EnvironmentObject private var selectionValue: ASSelectionValue 40 | @EnvironmentObject private var positionValue: ASPositionValue 41 | @Namespace private var namespace 42 | 43 | var item: ASItem { 44 | return ASItem(tag: tag, selectArea: selectArea, select: AnyView(select)) 45 | } 46 | 47 | var tag: SelectionValue 48 | var selectArea: CGFloat 49 | var select: S? = nil 50 | 51 | private var selection: Binding { Binding( 52 | get: { self.selectionValue.selection }, 53 | set: { 54 | self.selectionValue.onTapReceive?($0) 55 | self.selectionValue.selection = $0 56 | self.setupStateValue() 57 | }) 58 | } 59 | 60 | var normalSize: CGSize { 61 | if positionValue.constant.axisMode == .horizontal { 62 | return CGSize(width: positionValue.getNormalArea(self.selection.wrappedValue), 63 | height: positionValue.size.height) 64 | }else { 65 | return CGSize(width: positionValue.size.width, 66 | height: positionValue.getNormalArea(self.selection.wrappedValue)) 67 | } 68 | } 69 | 70 | var selectSize: CGSize { 71 | if positionValue.constant.axisMode == .horizontal { 72 | return CGSize(width: positionValue.getSelectArea(self.selection.wrappedValue), 73 | height: positionValue.size.height) 74 | }else { 75 | return CGSize(width: positionValue.size.width, 76 | height: positionValue.getSelectArea(self.selection.wrappedValue)) 77 | } 78 | } 79 | 80 | var divideLineView: some View { 81 | ZStack { 82 | if positionValue.constant.axisMode == .horizontal { 83 | Rectangle() 84 | .fill(positionValue.constant.divideLine.color) 85 | .frame(width: positionValue.constant.divideLine.width, height: positionValue.size.height) 86 | .scaleEffect(CGSize(width: 1, height: positionValue.constant.divideLine.scale)) 87 | }else { 88 | Rectangle() 89 | .fill(positionValue.constant.divideLine.color) 90 | .frame(width: positionValue.size.width, height: positionValue.constant.divideLine.width) 91 | .scaleEffect(CGSize(width: positionValue.constant.divideLine.scale, height: 1)) 92 | } 93 | } 94 | } 95 | 96 | func body(content: Content) -> some View { 97 | let item = ASItem(tag: tag, selectArea: selectArea, select: AnyView(select)) 98 | ZStack(alignment: .leading) { 99 | if positionValue.isHasStyle { 100 | Button { 101 | self.selection.wrappedValue = tag 102 | self.stateValue.isInitialRun = false 103 | if positionValue.constant.isActivatedVibration { vibration() } 104 | } label: { 105 | ZStack(alignment: positionValue.constant.axisMode == .horizontal ? .leading : .top) { 106 | getItemView(content) 107 | .frame(width: getItemSize().width, height: getItemSize().height) 108 | } 109 | .contentShape(Rectangle()) 110 | } 111 | .buttonStyle(.plain) 112 | .preference(key: ASItemPreferenceKey.self, value: [item]) 113 | }else { 114 | ZStack { 115 | Button { 116 | self.selection.wrappedValue = tag 117 | self.stateValue.isInitialRun = false 118 | if positionValue.constant.isActivatedVibration { vibration() } 119 | } label: { 120 | content 121 | } 122 | .contentShape(Rectangle()) 123 | .buttonStyle(.plain) 124 | .opacity(tag != self.selection.wrappedValue ? 1 : 0) 125 | 126 | select? 127 | .contentShape(Rectangle()) 128 | .opacity(tag == self.selection.wrappedValue ? 1 : 0) 129 | 130 | } 131 | .frame(width: getItemSize().width, height: getItemSize().height) 132 | .preference(key: ASItemPreferenceKey.self, value: [item]) 133 | } 134 | 135 | if isShowDivideLine() { 136 | ZStack { 137 | if positionValue.constant.axisMode == .horizontal { 138 | divideLineView 139 | .offset(x: -positionValue.constant.divideLine.width * 0.5) 140 | .matchedGeometryEffect(id: "DivideLine", in: namespace) 141 | }else { 142 | divideLineView 143 | .offset(y: -positionValue.constant.divideLine.width * 0.5) 144 | .matchedGeometryEffect(id: "DivideLine", in: namespace) 145 | } 146 | } 147 | } 148 | } 149 | .onAppear { 150 | self.setupStateValue() 151 | } 152 | .onChange(of: selectArea) { newValue in 153 | positionValue.toggleSelectArea.toggle() 154 | } 155 | 156 | } 157 | 158 | private func getItemSize() -> CGSize { 159 | if positionValue.constant.axisMode == .horizontal { 160 | return CGSize(width: tag == self.selection.wrappedValue ? selectSize.width : normalSize.width, height: positionValue.size.height) 161 | }else { 162 | return CGSize(width: positionValue.size.width, height: tag == self.selection.wrappedValue ? selectSize.height : normalSize.height) 163 | } 164 | } 165 | 166 | private func isShowDivideLine() -> Bool { 167 | let currentIndex = positionValue.indexOfTag(tag) 168 | let selectionIndex = positionValue.indexOfTag(self.selection.wrappedValue) 169 | if positionValue.constant.divideLine.isShowSelectionLine { 170 | return currentIndex != 0 171 | }else { 172 | return currentIndex != 0 && currentIndex != selectionIndex && currentIndex != selectionIndex + 1 173 | } 174 | } 175 | 176 | private func getItemView(_ content: Content) -> some View { 177 | ZStack { 178 | if tag == self.selection.wrappedValue { 179 | ZStack { 180 | if let select = select { 181 | select 182 | .onAppear { 183 | DispatchQueue.main.async { 184 | stateValue.previousIndex = stateValue.selectionIndex 185 | stateValue.previousFrame = positionValue.getSelectFrame(self.selection.wrappedValue, selectionIndex: stateValue.selectionIndex) 186 | 187 | if positionValue.constant.axisMode == .horizontal { 188 | stateValue.otherSize = CGSize(width: normalSize.width, height: positionValue.size.height) 189 | }else { 190 | stateValue.otherSize = CGSize(width: positionValue.size.width, height: normalSize.height) 191 | } 192 | } 193 | } 194 | .fixedSize() 195 | .matchedGeometryEffect(id: stateValue.constant.isActivatedGeometryEffect ? "ITEM-NAMESPACE-ID" : "\(UUID())", in: namespace) 196 | }else { 197 | content 198 | .fixedSize() 199 | .matchedGeometryEffect(id: stateValue.constant.isActivatedGeometryEffect ? "ITEM-NAMESPACE-ID" : "\(UUID())", in: namespace) 200 | } 201 | } 202 | } else { 203 | content 204 | .fixedSize() 205 | .matchedGeometryEffect(id: stateValue.constant.isActivatedGeometryEffect ? "ITEM-NAMESPACE-ID" : "\(UUID())", in: namespace) 206 | } 207 | } 208 | } 209 | 210 | private func setupStateValue() { 211 | let selectionIndex = positionValue.indexOfTag(self.selection.wrappedValue) 212 | stateValue.constant = positionValue.constant 213 | stateValue.itemCount = positionValue.itemCount 214 | stateValue.selectionIndex = selectionIndex 215 | stateValue.selectionFrame = positionValue.getSelectFrame(self.selection.wrappedValue, selectionIndex: selectionIndex) 216 | stateValue.size = positionValue.size 217 | } 218 | 219 | #if os(iOS) 220 | /// The device generates vibrations. 221 | /// - Parameter style: Vibration style. 222 | private func vibration(_ style: UIImpactFeedbackGenerator.FeedbackStyle = .soft) { 223 | let feedback = UIImpactFeedbackGenerator(style: style) 224 | feedback.prepare() 225 | feedback.impactOccurred() 226 | } 227 | #else 228 | private func vibration() {} 229 | #endif 230 | } 231 | 232 | extension ASItemModifier where SelectionValue: Hashable, S: View { 233 | init(tag: SelectionValue, selectArea: CGFloat, select: S) { 234 | self.tag = tag 235 | self.selectArea = selectArea 236 | self.select = select 237 | } 238 | } 239 | 240 | extension ASItemModifier where SelectionValue: Hashable, S == EmptyView { 241 | init(tag: SelectionValue, selectArea: CGFloat) { 242 | self.tag = tag 243 | self.selectArea = selectArea 244 | } 245 | } 246 | 247 | struct ASItemModifier_Previews: PreviewProvider { 248 | static var previews: some View { 249 | SegmentedViewPreview(constant: .init(axisMode: .horizontal, divideLine: .init(color: .blue.opacity(0.5)))) { 250 | ASBasicStyle() 251 | } 252 | .frame(height: 44) 253 | .padding(.horizontal, 16) 254 | .preferredColorScheme(.dark) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Private/ViewModel/ASPositionValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASPositionValue.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/18. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// A viewmodel that provides the state value of the tab button. 29 | class ASPositionValue: ObservableObject { 30 | 31 | @Published var size: CGSize = .zero 32 | @Published var items: [ASItem] = [] 33 | @Published var toggleSelectArea: Bool = false 34 | @Published var isHasStyle: Bool = true 35 | @Published var constant: ASConstant = .init() 36 | 37 | var itemCount: Int { 38 | return items.count 39 | } 40 | 41 | var sizeArea: CGFloat { 42 | constant.axisMode == .horizontal ? size.width : size.height 43 | } 44 | 45 | func indexOfTag(_ tag: SelectionValue?) -> Int { 46 | guard let tag = tag else { return 0 } 47 | return items.firstIndex(where: {$0.tag as! SelectionValue == tag}) ?? 0 48 | } 49 | 50 | func getNormalArea(_ selection: SelectionValue?) -> CGFloat { 51 | let selectArea = getItemSelectArea(selection) 52 | let area: CGFloat = selectArea > 0 ? ((sizeArea - selectArea) / CGFloat(itemCount - 1)) : sizeArea / CGFloat(itemCount) 53 | return area > 0 ? area : 1 54 | } 55 | 56 | func getSelectArea(_ selection: SelectionValue?) -> CGFloat { 57 | let selectArea = getItemSelectArea(selection) 58 | let area: CGFloat = selectArea > 0 ? selectArea : sizeArea / CGFloat(itemCount) 59 | return area > 0 ? area : 1 60 | } 61 | 62 | func getSelectFrame(_ selection: SelectionValue?, selectionIndex: Int) -> CGRect { 63 | let normalArea = getNormalArea(selection) 64 | let selectArea = getSelectArea(selection) 65 | 66 | if constant.axisMode == .horizontal { 67 | return CGRect(x: normalArea * CGFloat(selectionIndex), y: 0, width: selectArea, height: size.height) 68 | }else { 69 | return CGRect(x: 0, y: normalArea * CGFloat(selectionIndex), width: size.width, height: selectArea) 70 | } 71 | } 72 | 73 | private func getItemSelectArea(_ selection: SelectionValue?) -> CGFloat { 74 | guard !items.isEmpty else { return 0 } 75 | let selectionIndex = indexOfTag(selection) 76 | return items[selectionIndex].selectArea 77 | } 78 | } 79 | 80 | struct ASPositionValue_previews: PreviewProvider { 81 | static var previews: some View { 82 | SegmentedViewPreview { 83 | ASBasicStyle() 84 | } 85 | .frame(height: 44) 86 | .padding(.horizontal, 16) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Private/ViewModel/ASSelectionValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASSelectionValue.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/18. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Manage the currently selected tab status. 29 | class ASSelectionValue: ObservableObject { 30 | 31 | /// The currently selected tap value. 32 | @Binding var selection: SelectionValue 33 | var onTapReceive: ((SelectionValue) -> Void)? 34 | 35 | /// Initializes `ASSelectionValue` 36 | /// - Parameters: 37 | /// - selection: The currently selected tap value. 38 | /// - onTapReceive: Method that treats the currently selected tab as imperative syntax. 39 | init(selection: Binding, onTapReceive: ((SelectionValue) -> Void)? = nil) { 40 | _selection = selection 41 | self.onTapReceive = onTapReceive 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Constants/ASConstant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASConstant.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/18. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Motion effect mode in style. 29 | public enum ASMovementMode: Hashable { 30 | case normal 31 | case viscosity 32 | } 33 | 34 | /// The object position mode in style. 35 | public enum ASEdgeMode: Hashable { 36 | case topLeading 37 | case bottomTrailing 38 | } 39 | 40 | /// The axis mode of the segmented view. 41 | public enum ASAxisMode: Hashable { 42 | case horizontal 43 | case vertical 44 | } 45 | 46 | /// Defines the settings for the segmented view. 47 | public struct ASConstant: Equatable { 48 | 49 | /// The axis mode of the segmented view. 50 | public var axisMode: ASAxisMode 51 | 52 | /// Defines the line between the tab buttons. 53 | public var divideLine: ASLineConstant 54 | 55 | /// Activate the geometry effect. 56 | public var isActivatedGeometryEffect: Bool 57 | 58 | /// Activate the device's vibration. Only iOS is supported. 59 | public var isActivatedVibration: Bool 60 | 61 | /// A transition when a tab is selected. 62 | public var transition: AnyTransition 63 | 64 | /// Animation when selecting a tab. 65 | public var animation: Animation? 66 | 67 | 68 | /// Initializes `ASConstant` 69 | /// - Parameters: 70 | /// - axisMode: The axis mode of the segmented view. 71 | /// - divideLine: Defines the line between the tab buttons. 72 | /// - isActivatedGeometryEffect: Whether to use a GeometryEffect when switching tab buttons. 73 | /// - isActivatedVibration: Activate the device's vibration. Only iOS is supported. 74 | /// - transition: A transition when a tab is selected. 75 | /// - animation: Animation when selecting a tab. 76 | public init(axisMode: ASAxisMode = .horizontal, 77 | divideLine: ASLineConstant = .init(), 78 | isActivatedGeometryEffect: Bool = true, 79 | isActivatedVibration: Bool = true, 80 | transition: AnyTransition = .opacity, 81 | animation: Animation? = .easeInOut) { 82 | self.axisMode = axisMode 83 | self.divideLine = divideLine 84 | self.isActivatedGeometryEffect = isActivatedGeometryEffect 85 | self.isActivatedVibration = isActivatedVibration 86 | self.transition = transition 87 | self.animation = animation 88 | } 89 | 90 | public static func == (lhs: ASConstant, rhs: ASConstant) -> Bool { 91 | lhs.axisMode == rhs.axisMode && 92 | lhs.divideLine == rhs.divideLine && 93 | lhs.isActivatedGeometryEffect == rhs.isActivatedGeometryEffect && 94 | lhs.isActivatedVibration == rhs.isActivatedVibration && 95 | lhs.animation == rhs.animation 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Constants/ASLineConstant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASLineConstant.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/20. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Defines the line between the tab buttons. 29 | public struct ASLineConstant: Equatable { 30 | 31 | /// The color of the line. 32 | public var color: Color 33 | 34 | /// The short axis length of the line. 35 | public var width: CGFloat 36 | 37 | /// The length scale of the line's long axis. 38 | public var scale: CGFloat 39 | 40 | /// Whether to show the line around the selected tab. 41 | public var isShowSelectionLine: Bool 42 | 43 | /// Initializes `ASLineConstant` 44 | /// - Parameters: 45 | /// - color: The color of the line. 46 | /// - width: The short axis length of the line. 47 | /// - scale: The length scale of the line's long axis. 48 | /// - isShowSelectionLine: Whether to show the line around the selected tab. 49 | public init(color: Color = .clear, width: CGFloat = 1, scale: CGFloat = 0.6, isShowSelectionLine: Bool = false) { 50 | self.color = color 51 | self.width = width 52 | self.scale = scale 53 | self.isShowSelectionLine = isShowSelectionLine 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Extensions/Animation+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Animation+Extensions.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/20. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | extension Animation { 29 | public static var axisSegmentedSpring = Animation.spring(response: 0.36, dampingFraction: 0.8, blendDuration: 0.8) 30 | } 31 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Extensions/View+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+Extensions.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/07. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | extension View { 29 | public func itemTag(_ tag: SelectionValue, selectArea: CGFloat = 0) -> some View { 30 | modifier(ASItemModifier(tag: tag, selectArea: selectArea)) 31 | } 32 | 33 | public func itemTag(_ tag: SelectionValue, 34 | selectArea: CGFloat = 0, 35 | @ViewBuilder select: @escaping () -> S) -> some View { 36 | modifier(ASItemModifier(tag: tag, selectArea: selectArea, select: select())) 37 | } 38 | 39 | public func outerShadow(offset: CGFloat = 2, radius: CGFloat = 2, opacity: CGFloat = 1, isDark: Bool = true) -> some View { 40 | modifier(NeumorphismOuterModifier(offset: offset, radius: radius, opacity: opacity, isDark: isDark)) 41 | } 42 | 43 | public func innerShadow(_ content: S, radius: CGFloat = 6, opacity: CGFloat = 1, isDark: Bool = true) -> some View { 44 | modifier(NeumorphismInnerModifier(shape: content, radius: radius, opacity: opacity, isDark: isDark)) 45 | } 46 | 47 | func reverseMask(_ mask: M) -> some View where M: View { 48 | self.mask( 49 | mask 50 | .background(Color.white) 51 | .foregroundColor(.black) 52 | .compositingGroup() 53 | .luminanceToAlpha() 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Modifiers/NeumorphismInnerModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeumorphismInnerModifier.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/19. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | public struct NeumorphismInnerModifier : ViewModifier { 29 | 30 | public var lightColor: Color = .clear 31 | public var darkColor: Color = .clear 32 | 33 | public var shape: T 34 | public var radius: CGFloat 35 | public var opacity: CGFloat 36 | public var isDark: Bool 37 | 38 | public init(shape: T, radius: CGFloat = 10, opacity: CGFloat = 1, isDark: Bool = true) { 39 | 40 | self.shape = shape 41 | self.radius = radius 42 | self.opacity = opacity 43 | self.isDark = isDark 44 | 45 | if isDark { 46 | lightColor = .white.opacity(0.35) 47 | darkColor = .black.opacity(1.0) 48 | }else { 49 | lightColor = .white.opacity(1.0) 50 | darkColor = .black.opacity(0.35) 51 | } 52 | } 53 | 54 | public func body(content: Content) -> some View { 55 | content 56 | .overlay( 57 | GeometryReader { proxy in 58 | self.shape.fill(self.lightColor.opacity(shadowOpacity())) 59 | .reverseMask( 60 | self.shape.offset(x: -self.shadowOffset(proxy), y: -self.shadowOffset(proxy)) 61 | ) 62 | .blur(radius: self.radius) 63 | .offset(x: self.shadowOffset(proxy) , y: self.shadowOffset(proxy)) 64 | .overlay( 65 | self.shape.fill(self.darkColor.opacity(shadowOpacity())) 66 | .reverseMask( 67 | self.shape.offset(x: self.shadowOffset(proxy), y: self.shadowOffset(proxy)) 68 | ) 69 | .blur(radius: self.radius) 70 | .offset(x: -self.shadowOffset(proxy) , y: -self.shadowOffset(proxy)) 71 | ) 72 | .mask(self.shape) 73 | } 74 | ) 75 | } 76 | 77 | private func shadowOpacity() -> CGFloat { 78 | return 3 * max(min(opacity, 1), 0) 79 | } 80 | 81 | private func shadowOffset(_ proxy: GeometryProxy) -> CGFloat { 82 | return (proxy.size.width < proxy.size.height ? proxy.size.width : proxy.size.height) * 0.02 83 | } 84 | } 85 | 86 | struct NeumorphismInnerModifier_Previews: PreviewProvider { 87 | static var previews: some View { 88 | SegmentedViewPreview(constant: .init(axisMode: .horizontal)) { 89 | ASNeumorphismStyle(cornerRadius: 16, isInner: false) 90 | } 91 | .frame(height: 44) 92 | .padding(.horizontal, 16) 93 | .preferredColorScheme(.dark) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Modifiers/NeumorphismOuterModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeumorphismOuterModifier.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/19. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | public struct NeumorphismOuterModifier: ViewModifier { 29 | 30 | public var lightColor : Color = .clear 31 | public var darkColor : Color = .clear 32 | public var offset: CGFloat 33 | public var radius : CGFloat 34 | public var opacity: CGFloat 35 | 36 | public init(offset: CGFloat = 2, radius: CGFloat = 2, opacity: CGFloat = 1, isDark: Bool = true) { 37 | 38 | self.offset = offset 39 | self.radius = radius 40 | self.opacity = opacity 41 | 42 | if isDark { 43 | lightColor = .white.opacity(0.10) 44 | darkColor = .black.opacity(0.30) 45 | }else { 46 | lightColor = .white.opacity(1.0) 47 | darkColor = .black.opacity(0.20) 48 | } 49 | } 50 | 51 | public func body(content: Content) -> some View { 52 | content 53 | .shadow(color: lightColor.opacity(shadowOpacity()), radius: radius, x: -offset, y: -offset) 54 | .shadow(color: darkColor.opacity(shadowOpacity()), radius: radius, x: offset, y: offset) 55 | } 56 | 57 | private func shadowOpacity() -> CGFloat { 58 | return 3 * max(min(opacity, 1), 0) 59 | } 60 | } 61 | 62 | struct NeumorphismOuterModifier_Previews: PreviewProvider { 63 | static var previews: some View { 64 | SegmentedViewPreview(constant: .init(axisMode: .vertical)) { 65 | ASNeumorphismStyle(cornerRadius: 25) 66 | } 67 | .frame(width: 50) 68 | .padding(.vertical, 16) 69 | .preferredColorScheme(.dark) 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Previews/SegmentedViewPreview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SegmentedViewPreview.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/18. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// This is a preview for testing. 29 | public struct SegmentedViewPreview: View where S : View { 30 | 31 | @State private var selection: Int = 0 32 | private let constant: ASConstant 33 | private let style: () -> S 34 | 35 | public init(constant: ASConstant = .init(), @ViewBuilder style: @escaping () -> S) { 36 | self.constant = constant 37 | self.style = style 38 | } 39 | 40 | public var body: some View { 41 | GeometryReader { proxy in 42 | ZStack { 43 | Color.clear 44 | AxisSegmentedView(selection: $selection, constant: constant, { 45 | iconGroup 46 | }, style: { 47 | style() 48 | }) 49 | .font(.system(size: 20)) 50 | } 51 | } 52 | } 53 | 54 | var iconGroup: some View { 55 | Group { 56 | Image(systemName: "align.horizontal.left") 57 | .itemTag(0, selectArea: 0) { 58 | Image(systemName: "align.horizontal.left.fill").foregroundColor(Color.white) 59 | } 60 | Image(systemName: "align.horizontal.right") 61 | .itemTag(1, selectArea: 0) { 62 | Image(systemName: "align.horizontal.right.fill").foregroundColor(Color.white) 63 | } 64 | Image(systemName: "align.vertical.top") 65 | .itemTag(2, selectArea: 160) { 66 | Image(systemName: "align.vertical.top.fill").foregroundColor(Color.white) 67 | } 68 | Image(systemName: "align.vertical.bottom") 69 | .itemTag(3, selectArea: 0) { 70 | Image(systemName: "align.vertical.bottom.fill").foregroundColor(Color.white) 71 | } 72 | } 73 | } 74 | 75 | var textGroup: some View { 76 | Group { 77 | Text("버튼1") 78 | .font(.callout) 79 | .frame(width: 60) 80 | .itemTag(0, selectArea: 0) { 81 | Text("버튼") 82 | .font(.callout) 83 | } 84 | Text("버튼2") 85 | .font(.callout) 86 | .itemTag(1, selectArea: 0) { 87 | Text("버튼22") 88 | .font(.callout) 89 | } 90 | Text("버튼3") 91 | .font(.callout) 92 | .itemTag(2, selectArea: 160) { 93 | Text("버튼33") 94 | .font(.callout) 95 | } 96 | Text("버튼4") 97 | .font(.callout) 98 | .itemTag(3, selectArea: 0) { 99 | Text("버튼44") 100 | .font(.callout) 101 | } 102 | } 103 | } 104 | } 105 | 106 | struct SegmentedViewPreview_previews: PreviewProvider { 107 | static var previews: some View { 108 | SegmentedViewPreview(constant: .init()) { 109 | ASBasicStyle() 110 | } 111 | .frame(height: 44) 112 | .padding(.horizontal, 16) 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Shapes/ASCurveShape.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASCurveShape.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/04. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// A shape that expresses a curve. 29 | public struct ASCurveShape: Shape { 30 | 31 | /// The diameter representing the curve. 32 | let radius: CGFloat 33 | 34 | /// A value indicating the depth of the curve. A value between -1 and 1. 35 | var depth: CGFloat 36 | 37 | /// The value of the horizontal position of the curve. A value between 0 and 1. 38 | var position: CGFloat 39 | 40 | /// Animate depth and position. 41 | public var animatableData: AnimatablePair { 42 | get { 43 | AnimatablePair(depth, position) 44 | } 45 | set { 46 | depth = max(min(newValue.first, 1), -1) 47 | position = max(min(newValue.second, 1), 0) 48 | } 49 | } 50 | 51 | /// Initializes `ATCurveShape` 52 | /// - Parameters: 53 | /// - radius: The diameter representing the curve. 54 | /// - depth: A value indicating the depth of the curve. A value between -1 and 1. 55 | /// - position: The value of the horizontal position of the curve. A value between 0 and 1. 56 | public init(radius: CGFloat, depth: CGFloat, position: CGFloat) { 57 | self.radius = radius 58 | self.depth = depth 59 | self.position = position 60 | } 61 | 62 | public func path(in rect: CGRect) -> Path { 63 | var path = Path() 64 | 65 | let delta = 1 + (1 - abs(depth)) 66 | let half = rect.width * position 67 | let curvePoint: CGPoint = CGPoint(x: radius / 1.74, y: (radius / 3.5) * depth) 68 | let edgeX: CGFloat = radius / ((2.85 - 0.36 * abs(depth)) - delta) 69 | 70 | path.move(to: CGPoint(x: half - radius , y: 0)) 71 | path.addQuadCurve(to: CGPoint(x: half - curvePoint.x, y: curvePoint.y), 72 | control: CGPoint(x: half - edgeX , y: 0)) 73 | path.addCurve(to: CGPoint(x: half + curvePoint.x, y: curvePoint.y), 74 | control1: CGPoint(x: half - radius / (delta * (3 + (1 - depth))), y: (radius / delta) * depth), 75 | control2: CGPoint(x: half + radius / (delta * (3 + (1 - depth))), y: (radius / delta) * depth)) 76 | path.addQuadCurve(to: CGPoint(x: half + radius, y: 0), 77 | control: CGPoint(x: half + edgeX, y: 0)) 78 | path.addLine(to: CGPoint(x: rect.width , y: 0)) 79 | path.addLine(to: CGPoint(x: rect.width , y: rect.height)) 80 | path.addLine(to: CGPoint(x: 0 , y: rect.height)) 81 | path.addLine(to: CGPoint(x: 0 , y: 0)) 82 | path.closeSubpath() 83 | return path 84 | } 85 | } 86 | 87 | struct ASCurveShape_Previews: PreviewProvider { 88 | static var previews: some View { 89 | ASCurveShape(radius: 60, depth: 0.8, position: 0.5) 90 | .stroke() 91 | .frame(width: 220, height: 60) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Styles/ASBasicStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASBasicStyle.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/12. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Basic style for segmented view. 29 | public struct ASBasicStyle: View { 30 | 31 | @EnvironmentObject var stateValue: ASStateValue 32 | 33 | public var backgroundColor: Color 34 | public var foregroundColor: Color 35 | public var cornerRadius: CGFloat 36 | public var padding: CGFloat 37 | public var isApplySelectionCornerRadius: Bool 38 | public var movementMode: ASMovementMode 39 | public var animation: Animation 40 | 41 | public init(backgroundColor: Color = .gray, 42 | foregroundColor: Color = Color.blue, 43 | cornerRadius: CGFloat = 6, 44 | padding: CGFloat = 3, 45 | isApplySelectionCornerRadius: Bool = true, 46 | movementMode: ASMovementMode = .viscosity, 47 | animation: Animation = .axisSegmentedSpring) { 48 | self.backgroundColor = backgroundColor 49 | self.foregroundColor = foregroundColor 50 | self.cornerRadius = cornerRadius 51 | self.padding = padding 52 | self.isApplySelectionCornerRadius = isApplySelectionCornerRadius 53 | self.movementMode = movementMode 54 | self.animation = animation 55 | } 56 | 57 | private var selectionView: some View { 58 | RoundedRectangle(cornerRadius: isApplySelectionCornerRadius ? cornerRadius : 0) 59 | .fill(foregroundColor) 60 | .padding(padding + 0.5) 61 | } 62 | 63 | public var body: some View { 64 | ZStack(alignment: .topLeading) { 65 | RoundedRectangle(cornerRadius: cornerRadius) 66 | .fill(backgroundColor.opacity(0.2)) 67 | .overlay( 68 | RoundedRectangle(cornerRadius: cornerRadius) 69 | .stroke(lineWidth: 1) 70 | .fill(foregroundColor) 71 | ) 72 | .padding(0.5) 73 | switch movementMode { 74 | case .normal: ASNormalStyle(animation) { _ in 75 | selectionView 76 | } 77 | case .viscosity: ASViscosityStyle { _ in 78 | selectionView 79 | } 80 | } 81 | } 82 | .mask(RoundedRectangle(cornerRadius: cornerRadius)) 83 | .animation(animation, value: stateValue.selectionIndex) 84 | } 85 | } 86 | 87 | struct ASBasicStyle_Previews: PreviewProvider { 88 | static var previews: some View { 89 | SegmentedViewPreview(constant: .init(divideLine: .init(color: .blue, width: 1, scale: 1))) { 90 | ASBasicStyle() 91 | .preferredColorScheme(.dark) 92 | } 93 | .frame(height: 44) 94 | .padding(.horizontal, 16) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Styles/ASCapsuleStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASCapsuleStyle.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/19. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Capsule style for segmented view. 29 | public struct ASCapsuleStyle: View { 30 | 31 | @EnvironmentObject var stateValue: ASStateValue 32 | 33 | public var backgroundColor: Color 34 | public var foregroundColor: Color 35 | public var movementMode: ASMovementMode 36 | public var animation: Animation 37 | 38 | public init(backgroundColor: Color = .gray.opacity(0.4), 39 | foregroundColor: Color = Color.blue, 40 | movementMode: ASMovementMode = .viscosity, 41 | animation: Animation = .axisSegmentedSpring) { 42 | self.backgroundColor = backgroundColor 43 | self.foregroundColor = foregroundColor 44 | self.movementMode = movementMode 45 | self.animation = animation 46 | } 47 | 48 | private var selectionView: some View { 49 | Capsule() 50 | .fill(foregroundColor) 51 | .padding(3) 52 | } 53 | 54 | public var body: some View { 55 | ZStack(alignment: .topLeading) { 56 | Capsule() 57 | .fill(backgroundColor.opacity(0.2)) 58 | Capsule() 59 | .stroke(lineWidth: 1) 60 | .fill(backgroundColor.opacity(0.2)) 61 | .padding(1) 62 | 63 | switch movementMode { 64 | case .normal: ASNormalStyle(animation) { _ in 65 | selectionView 66 | } 67 | case .viscosity: ASViscosityStyle(animation) { _ in 68 | selectionView 69 | } 70 | } 71 | } 72 | .animation(animation, value: stateValue.selectionIndex) 73 | } 74 | } 75 | 76 | struct ASCapsuleStyle_Previews: PreviewProvider { 77 | static var previews: some View { 78 | SegmentedViewPreview { 79 | ASCapsuleStyle() 80 | .preferredColorScheme(.dark) 81 | } 82 | .frame(height: 44) 83 | .padding(.horizontal, 16) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Styles/ASJellyStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASJellyStyle.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/22. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Jelly style for segmented view. 29 | public struct ASJellyStyle: View { 30 | 31 | @EnvironmentObject var stateValue: ASStateValue 32 | 33 | public var backgroundColor: Color 34 | public var foregroundColor: Color 35 | public var jellyRadius: CGFloat 36 | public var jellyDepth: CGFloat 37 | public var jellyEdge: ASEdgeMode 38 | public var animation: Animation 39 | 40 | @State private var currentFrame: CGRect = .zero 41 | @State private var depth: CGFloat = 0 42 | 43 | public init(backgroundColor: Color = .gray.opacity(0.1), 44 | foregroundColor: Color = .purple, 45 | jellyRadius: CGFloat = 56, 46 | jellyDepth: CGFloat = 0.9, 47 | jellyEdge: ASEdgeMode = .bottomTrailing, 48 | animation: Animation = .axisSegmentedSpring) { 49 | self.backgroundColor = backgroundColor 50 | self.foregroundColor = foregroundColor 51 | self.jellyRadius = jellyRadius 52 | self.jellyDepth = jellyDepth 53 | self.jellyEdge = jellyEdge 54 | self.animation = animation 55 | } 56 | 57 | private var jellyView: some View { 58 | ZStack { 59 | if stateValue.constant.axisMode == .horizontal { 60 | ZStack { 61 | Color.clear 62 | Rectangle() 63 | .fill(backgroundColor) 64 | Rectangle() 65 | .fill(foregroundColor) 66 | .reverseMask( 67 | ASCurveShape(radius: jellyRadius, depth: depth, position: currentFrame.midX / stateValue.size.width) 68 | .fill(.black) 69 | .scaleEffect(CGSize(width: 1, height: jellyEdge == .bottomTrailing ? -1 : 1)) 70 | ) 71 | .mask( 72 | Rectangle() 73 | .fill(foregroundColor) 74 | ) 75 | } 76 | }else { 77 | ZStack { 78 | Color.clear 79 | Rectangle() 80 | .fill(backgroundColor) 81 | .frame(width: stateValue.size.width) 82 | Rectangle() 83 | .fill(foregroundColor) 84 | .frame(width: stateValue.size.height + 1, height: stateValue.size.width + 1) 85 | .reverseMask( 86 | ASCurveShape(radius: jellyRadius, depth: depth, position: currentFrame.midY / stateValue.size.height) 87 | .fill(.black) 88 | ) 89 | .mask( 90 | Rectangle() 91 | .fill(foregroundColor) 92 | ) 93 | .rotationEffect(Angle(degrees: 90)) 94 | .scaleEffect(CGSize(width: jellyEdge == .bottomTrailing ? 1 : -1, height: 1)) 95 | } 96 | } 97 | } 98 | .onAppear { 99 | self.currentFrame = stateValue.selectionFrame 100 | } 101 | } 102 | 103 | public var body: some View { 104 | ZStack(alignment: .topLeading) { 105 | jellyView 106 | ASNormalStyle(animation) { rect in 107 | Color.clear 108 | .onChange(of: rect) { newValue in 109 | updateUI() 110 | withAnimation(animation) { 111 | self.currentFrame = newValue 112 | } 113 | } 114 | } 115 | } 116 | .onChange(of: jellyDepth, perform: { newValue in 117 | depth = newValue 118 | }) 119 | .animation(animation, value: stateValue.selectionIndex) 120 | } 121 | 122 | private func updateUI() { 123 | withAnimation(.easeInOut(duration: 0.16)) { 124 | depth = 0.4 125 | } 126 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 127 | withAnimation(.spring(response: 0.4, dampingFraction: 0.6, blendDuration: 0.5)) { 128 | depth = jellyDepth 129 | } 130 | } 131 | } 132 | } 133 | 134 | struct ASJellyStyle_Previews: PreviewProvider { 135 | static var previews: some View { 136 | SegmentedViewPreview(constant: .init(axisMode: .horizontal)) { 137 | ASJellyStyle() 138 | } 139 | .frame(height: 44) 140 | .padding(.horizontal, 16) 141 | .preferredColorScheme(.dark) 142 | // SegmentedViewPreview(constant: .init(axisMode: .vertical)) { 143 | // ASJellyStyle() 144 | // } 145 | // .frame(width: 50) 146 | // .padding(.horizontal, 16) 147 | // .preferredColorScheme(.dark) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Styles/ASLineStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASLineStyle.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/19. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Line style for segmented view. 29 | public struct ASLineStyle: View { 30 | 31 | @EnvironmentObject var stateValue: ASStateValue 32 | 33 | public var lineColor: Color 34 | public var lineSmallWidth: CGFloat 35 | public var lineLargeScale: CGFloat 36 | public var lineEdge: ASEdgeMode 37 | public var movementMode: ASMovementMode 38 | public var animation: Animation 39 | 40 | public init(lineColor: Color = .blue, 41 | lineSmallWidth: CGFloat = 2, 42 | lineLargeScale: CGFloat = 1, 43 | lineEdge: ASEdgeMode = .bottomTrailing, 44 | movementMode: ASMovementMode = .viscosity, 45 | animation: Animation = .axisSegmentedSpring) { 46 | self.lineColor = lineColor 47 | self.lineSmallWidth = lineSmallWidth 48 | self.lineLargeScale = lineLargeScale 49 | self.lineEdge = lineEdge 50 | self.movementMode = movementMode 51 | self.animation = animation 52 | } 53 | 54 | private var selectionView: some View { 55 | return Group { 56 | if lineEdge == .bottomTrailing { 57 | Spacer() 58 | } 59 | ZStack(alignment: .topLeading) { 60 | if stateValue.constant.axisMode == .horizontal { 61 | Rectangle() 62 | .fill(lineColor) 63 | .frame(height: lineSmallWidth) 64 | .scaleEffect(CGSize(width: lineLargeScale, height: 1)) 65 | }else { 66 | Rectangle() 67 | .fill(lineColor) 68 | .frame(width: lineSmallWidth) 69 | .scaleEffect(CGSize(width: 1, height: lineLargeScale)) 70 | } 71 | } 72 | if lineEdge == .topLeading { 73 | Spacer() 74 | } 75 | } 76 | } 77 | 78 | public var body: some View { 79 | ZStack(alignment: .topLeading) { 80 | Color.clear 81 | if stateValue.constant.axisMode == .horizontal { 82 | switch movementMode { 83 | case .normal: ASNormalStyle(animation) { _ in 84 | VStack(spacing: 0) { 85 | selectionView 86 | } 87 | } 88 | case .viscosity: ASViscosityStyle(animation) { _ in 89 | VStack(spacing: 0) { 90 | selectionView 91 | } 92 | } 93 | } 94 | }else { 95 | switch movementMode { 96 | case .normal: ASNormalStyle(animation) { _ in 97 | HStack(spacing: 0) { 98 | selectionView 99 | } 100 | } 101 | case .viscosity: ASViscosityStyle(animation) { _ in 102 | HStack(spacing: 0) { 103 | selectionView 104 | } 105 | } 106 | } 107 | } 108 | } 109 | .animation(animation, value: stateValue.selectionIndex) 110 | } 111 | } 112 | 113 | struct ASLineStyle_Previews: PreviewProvider { 114 | static var previews: some View { 115 | SegmentedViewPreview(constant: .init(axisMode: .horizontal, divideLine: .init(color: .blue.opacity(0.5), isShowSelectionLine: true))) { 116 | ASLineStyle(lineEdge: .bottomTrailing) 117 | .preferredColorScheme(.dark) 118 | } 119 | .frame(height: 44) 120 | .padding(.horizontal, 16) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Styles/ASNeumorphismStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASNeumorphismStyle.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/19. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Neumorphism style for segmented view. 29 | public struct ASNeumorphismStyle: View { 30 | 31 | @Environment(\.colorScheme) private var colorScheme 32 | @EnvironmentObject var stateValue: ASStateValue 33 | 34 | public var backgroundColor: Color 35 | public var foregroundColor: Color 36 | public var cornerRadius: CGFloat 37 | public var padding: CGFloat 38 | public var shadowRadius: CGFloat 39 | public var shadowOpacity: CGFloat 40 | public var isInner: Bool 41 | public var movementMode: ASMovementMode 42 | public var animation: Animation 43 | 44 | public init(backgroundColor: Color = .clear, 45 | foregroundColor: Color = .clear, 46 | cornerRadius: CGFloat = 11, 47 | padding: CGFloat = 3, 48 | shadowRadius: CGFloat = 5, 49 | shadowOpacity: CGFloat = 1, 50 | isInner: Bool = false, 51 | movementMode: ASMovementMode = .viscosity, 52 | animation: Animation = .axisSegmentedSpring) { 53 | self.backgroundColor = backgroundColor 54 | self.foregroundColor = foregroundColor 55 | self.cornerRadius = cornerRadius 56 | self.padding = padding 57 | self.shadowRadius = shadowRadius 58 | self.shadowOpacity = shadowOpacity 59 | self.isInner = isInner 60 | self.movementMode = movementMode 61 | self.animation = animation 62 | } 63 | 64 | public var body: some View { 65 | ZStack { 66 | if isInner { 67 | innerView 68 | }else { 69 | outerView 70 | } 71 | } 72 | .animation(animation, value: stateValue.selectionIndex) 73 | } 74 | 75 | private var innerView: some View { 76 | ZStack(alignment: .topLeading) { 77 | RoundedRectangle(cornerRadius: cornerRadius) 78 | .fill(getBackgroundColor()) 79 | .outerShadow(radius: shadowRadius, opacity: shadowOpacity, isDark: colorScheme == .dark) 80 | 81 | switch movementMode { 82 | case .normal: ASNormalStyle(animation) { _ in 83 | selectionView 84 | } 85 | case .viscosity: ASViscosityStyle(animation) { _ in 86 | selectionView 87 | } 88 | } 89 | } 90 | .mask(RoundedRectangle(cornerRadius: cornerRadius)) 91 | } 92 | 93 | private var outerView: some View { 94 | ZStack(alignment: .topLeading) { 95 | RoundedRectangle(cornerRadius: cornerRadius) 96 | .fill(getBackgroundColor()) 97 | .innerShadow(RoundedRectangle(cornerRadius: cornerRadius), radius: shadowRadius, opacity: shadowOpacity, isDark: colorScheme == .dark) 98 | switch movementMode { 99 | case .normal: ASNormalStyle(animation) { _ in 100 | selectionView 101 | } 102 | case .viscosity: ASViscosityStyle(animation) { _ in 103 | selectionView 104 | } 105 | } 106 | } 107 | .mask(RoundedRectangle(cornerRadius: cornerRadius)) 108 | } 109 | 110 | private var selectionView: some View { 111 | ZStack { 112 | if isInner { 113 | RoundedRectangle(cornerRadius: cornerRadius) 114 | .fill(getForegroundColor()) 115 | .innerShadow(RoundedRectangle(cornerRadius: cornerRadius), radius: shadowRadius, opacity: shadowOpacity, isDark: colorScheme == .dark) 116 | .padding(padding) 117 | }else { 118 | RoundedRectangle(cornerRadius: cornerRadius) 119 | .fill(getForegroundColor()) 120 | .padding(padding) 121 | .outerShadow(radius: shadowRadius, opacity: shadowOpacity, isDark: colorScheme == .dark) 122 | } 123 | } 124 | } 125 | 126 | private func getBackgroundColor() -> Color { 127 | return backgroundColor == .clear ? (colorScheme == .dark ? Color(red: 0.185, green: 0.190, blue: 0.202) : Color(red: 0.926, green: 0.942, blue: 0.952)) : backgroundColor 128 | } 129 | 130 | private func getForegroundColor() -> Color { 131 | return foregroundColor == .clear ? (colorScheme == .dark ? Color(red: 0.185, green: 0.190, blue: 0.202) : Color(red: 0.926, green: 0.942, blue: 0.952)) : foregroundColor 132 | } 133 | } 134 | 135 | struct ASNeumorphismStyle_Previews: PreviewProvider { 136 | static var previews: some View { 137 | SegmentedViewPreview(constant: .init(axisMode: .horizontal)) { 138 | ASNeumorphismStyle(backgroundColor: .blue.opacity(0.5), cornerRadius: 25, shadowOpacity: 1, isInner: false) 139 | .preferredColorScheme(.dark) 140 | } 141 | .frame(height: 44) 142 | .padding(.horizontal, 16) 143 | } 144 | } 145 | 146 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Styles/ASScaleStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASScaleStyle.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/20. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Scale style for segmented view. 29 | public struct ASScaleStyle: View { 30 | 31 | @EnvironmentObject var stateValue: ASStateValue 32 | 33 | public var backgroundColor: Color 34 | public var foregroundColor: Color 35 | public var cornerRadius: CGFloat 36 | public var minimumScale: CGFloat 37 | public var animation: Animation 38 | 39 | public init(backgroundColor: Color = .clear, 40 | foregroundColor: Color = Color.blue, 41 | cornerRadius: CGFloat = 11, 42 | minimumScale: CGFloat = 0.1, 43 | animation: Animation = .axisSegmentedSpring) { 44 | self.backgroundColor = backgroundColor 45 | self.foregroundColor = foregroundColor 46 | self.cornerRadius = cornerRadius 47 | self.minimumScale = minimumScale 48 | self.animation = animation 49 | } 50 | 51 | @State private var scale: CGFloat = 1 52 | @State private var alpha: CGFloat = 1 53 | 54 | private var content: some View { 55 | ASNormalStyle(animation) { _ in 56 | RoundedRectangle(cornerRadius: cornerRadius) 57 | .fill(foregroundColor) 58 | .padding(3) 59 | .scaleEffect(scale) 60 | .opacity(alpha) 61 | .transition(.opacity) 62 | } 63 | } 64 | 65 | public var body: some View { 66 | ZStack(alignment: .topLeading) { 67 | Color.clear 68 | content 69 | } 70 | .background(backgroundColor) 71 | .onChange(of: stateValue.selectionIndex) { newValue in 72 | withAnimation(.easeInOut(duration: 0.3)) { 73 | scale = minimumScale 74 | alpha = 0 75 | } 76 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 77 | withAnimation(animation) { 78 | scale = 1.0 79 | alpha = 1.0 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | struct ASScaleStyle_Previews: PreviewProvider { 87 | static var previews: some View { 88 | SegmentedViewPreview(constant: .init(axisMode: .horizontal, divideLine: .init(color: .blue.opacity(0.4), scale: 0.34))) { 89 | ASScaleStyle(cornerRadius: 5) 90 | } 91 | .frame(height: 44) 92 | .padding(.horizontal, 16) 93 | .preferredColorScheme(.dark) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Styles/Movement/ASNormalStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASNormalStyle.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/22. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Normal movement style for segmented view. 29 | public struct ASNormalStyle: View where Content: View { 30 | 31 | @EnvironmentObject var stateValue: ASStateValue 32 | @State private var currentFrame: CGRect = .zero 33 | 34 | public var animation: Animation 35 | public var selectionView: (CGRect) -> Content 36 | 37 | public init(_ animation: Animation = .axisSegmentedSpring, 38 | @ViewBuilder selectionView: @escaping (CGRect) -> Content) { 39 | self.animation = animation 40 | self.selectionView = selectionView 41 | } 42 | 43 | private var content: some View { 44 | let rect = stateValue.selectionFrame 45 | return selectionView(currentFrame) 46 | .frame(width: rect.width, height: rect.height) 47 | .offset(x: rect.origin.x, y: rect.origin.y) 48 | } 49 | 50 | public var body: some View { 51 | ZStack(alignment: .topLeading) { 52 | Color.clear 53 | content 54 | } 55 | .onAppear { 56 | currentFrame = stateValue.selectionFrame 57 | } 58 | .onChange(of: stateValue.selectionFrame) { newValue in 59 | if stateValue.isInitialRun { 60 | currentFrame = newValue 61 | }else { 62 | withAnimation(animation) { 63 | currentFrame = newValue 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | struct ASNormalStyle_Previews: PreviewProvider { 71 | static var previews: some View { 72 | SegmentedViewPreview(constant: .init(axisMode: .horizontal, divideLine: .init(color: .blue.opacity(0.4), scale: 0.34))) { 73 | ASNormalStyle { _ in 74 | RoundedRectangle(cornerRadius: 11) 75 | .stroke(lineWidth: 1) 76 | .fill(Color.blue) 77 | .padding(1) 78 | } 79 | .preferredColorScheme(.dark) 80 | } 81 | .frame(height: 44) 82 | .padding(.horizontal, 16) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/Styles/Movement/ASViscosityStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASViscosityStyle.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/22. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// Viscosity movement style for segmented view. 29 | public struct ASViscosityStyle: View where Content: View { 30 | 31 | @EnvironmentObject var stateValue: ASStateValue 32 | @State private var currentFrame: CGRect = .zero 33 | 34 | public var animation: Animation 35 | public var selectionView: (CGRect) -> Content 36 | 37 | public init(_ animation: Animation = .axisSegmentedSpring, 38 | @ViewBuilder selectionView: @escaping (CGRect) -> Content) { 39 | self.animation = animation 40 | self.selectionView = selectionView 41 | } 42 | 43 | private var content: some View { 44 | selectionView(currentFrame) 45 | .frame(width: currentFrame.width, height: currentFrame.height) 46 | .offset(x: currentFrame.origin.x, y: currentFrame.origin.y) 47 | } 48 | 49 | public var body: some View { 50 | ZStack(alignment: .topLeading) { 51 | Color.clear 52 | content 53 | } 54 | .onAppear { 55 | currentFrame = stateValue.selectionFrame 56 | } 57 | .onChange(of: stateValue.selectionFrame) { newValue in 58 | if stateValue.isInitialRun { 59 | currentFrame = newValue 60 | }else { 61 | withAnimation(.easeInOut(duration: 0.24)) { 62 | currentFrame = getExpandRect() 63 | } 64 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.24) { 65 | withAnimation(animation) { 66 | currentFrame = newValue 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | private func getExpandRect() -> CGRect { 74 | let selectionFrame = stateValue.selectionFrame 75 | let previousFrame = stateValue.previousFrame 76 | if stateValue.previousIndex < stateValue.selectionIndex { 77 | if stateValue.constant.axisMode == .horizontal { 78 | return CGRect(x: currentFrame.origin.x, 79 | y: currentFrame.origin.y, 80 | width: selectionFrame.origin.x + selectionFrame.width - currentFrame.origin.x, 81 | height: selectionFrame.height) 82 | }else { 83 | return CGRect(x: currentFrame.origin.x, 84 | y: currentFrame.origin.y, 85 | width: selectionFrame.width, 86 | height: selectionFrame.origin.y + selectionFrame.height - currentFrame.origin.y) 87 | } 88 | }else { 89 | if stateValue.constant.axisMode == .horizontal { 90 | return CGRect(x: selectionFrame.origin.x, 91 | y: selectionFrame.origin.y, 92 | width: currentFrame.origin.x + currentFrame.width - selectionFrame.origin.x, 93 | height: previousFrame.height) 94 | }else { 95 | return CGRect(x: selectionFrame.origin.x, 96 | y: selectionFrame.origin.y, 97 | width: previousFrame.width, 98 | height: currentFrame.origin.y + currentFrame.height - selectionFrame.origin.y) 99 | } 100 | } 101 | } 102 | } 103 | 104 | struct ASViscosityStyle_Previews: PreviewProvider { 105 | static var previews: some View { 106 | SegmentedViewPreview(constant: .init(axisMode: .horizontal, divideLine: .init(color: .blue.opacity(0.4), scale: 0.34))) { 107 | ASViscosityStyle { rect in 108 | RoundedRectangle(cornerRadius: 11) 109 | .stroke(lineWidth: 1) 110 | .fill(Color.blue) 111 | .padding(1) 112 | } 113 | .preferredColorScheme(.dark) 114 | } 115 | .frame(height: 44) 116 | .padding(.horizontal, 16) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/AxisSegmentedView/Public/ViewModel/ASStateValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASStateValue.swift 3 | // AxisSegmentedView 4 | // 5 | // Created by jasu on 2022/03/19. 6 | // Copyright (c) 2022 jasu All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is furnished 13 | // to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 19 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 22 | // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 23 | // OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | // 25 | 26 | import SwiftUI 27 | 28 | /// An environment viewmodel that can be referenced by styles. 29 | public class ASStateValue: ObservableObject { 30 | 31 | /// Check if it is in the first running state. 32 | /// Used to remove animation at first startup. 33 | public var isInitialRun: Bool = true 34 | 35 | /// A constant value for segmented view. 36 | @Published public var constant: ASConstant = .init() 37 | 38 | /// Total number of tab buttons. 39 | @Published public var itemCount: Int = 0 40 | 41 | /// The previously selected position index value. 42 | @Published public var previousIndex: Int = 0 43 | 44 | /// The previously selected position frame value. 45 | @Published public var previousFrame: CGRect = .zero 46 | 47 | /// The currently selected position index value. 48 | @Published public var selectionIndex: Int = 0 49 | 50 | /// The currently selected position frame value. 51 | @Published public var selectionFrame: CGRect = .zero 52 | 53 | /// The size of the tab button that is not currently selected. 54 | @Published public var otherSize: CGSize = .zero 55 | 56 | /// The full size of the segmented view. 57 | @Published public var size: CGSize = .zero 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /Tests/AxisSegmentedViewTests/AxisSegmentedViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import AxisSegmentedView 3 | 4 | final class AxisSegmentedViewTests: XCTestCase { 5 | func testExample() throws { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(AxisSegmentedView().text, "Hello, World!") 10 | } 11 | } 12 | --------------------------------------------------------------------------------