├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
└── Sources
└── SwiftUISideMenu
└── SwiftUISideMenu.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 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Gordan Glavaš
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 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
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: "SwiftUISideMenu",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, and make them visible to other packages.
13 | .library(
14 | name: "SwiftUISideMenu",
15 | targets: ["SwiftUISideMenu"]),
16 | ],
17 | dependencies: [
18 | // Dependencies declare other packages that this package depends on.
19 | // .package(url: /* package url */, from: "1.0.0"),
20 | ],
21 | targets: [
22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
23 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
24 | .target(
25 | name: "SwiftUISideMenu",
26 | dependencies: []),
27 | .testTarget(
28 | name: "SwiftUISideMenuTests",
29 | dependencies: ["SwiftUISideMenu"]),
30 | ]
31 | )
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUISideMenu
2 |
3 | Add a **Side Menu / Navigation Drawer** to your SwiftUI view! The side menu is animated, responds to user swipes and can be manually shown or hidden.
4 |
5 | The end result looks like this:
6 |
7 | 
8 |
9 | ### Recipe
10 |
11 | Check out [this recipe](https://swiftuirecipes.com/blog/side-menu-in-swiftui) for in-depth description of the component and its code. Check out [SwiftUIRecipes.com](https://swiftuirecipes.com) for more **SwiftUI recipes**!
12 |
13 | ### Sample usage
14 |
15 | ```swift
16 | struct SideMenuTest: View {
17 | @State private var showSideMenu = false
18 |
19 | var body: some View {
20 | NavigationView {
21 | List(1..<6) { index in
22 | Text("Item \(index)")
23 | }
24 | .navigationBarTitle("Dashboard", displayMode: .inline)
25 | .navigationBarItems(leading: (
26 | Button(action: {
27 | withAnimation {
28 | self.showSideMenu.toggle()
29 | }
30 | }) {
31 | Image(systemName: "line.horizontal.3")
32 | .imageScale(.large)
33 | }
34 | ))
35 | }.sideMenu(isShowing: $showSideMenu) {
36 | VStack(alignment: .leading) {
37 | Button(action: {
38 | withAnimation {
39 | self.showSideMenu = false
40 | }
41 | }) {
42 | HStack {
43 | Image(systemName: "xmark")
44 | .foregroundColor(.white)
45 | Text("close menu")
46 | .foregroundColor(.white)
47 | .font(.system(size: 14))
48 | .padding(.leading, 15.0)
49 | }
50 | }.padding(.top, 20)
51 | Divider()
52 | .frame(height: 20)
53 | Text("Sample item 1")
54 | .foregroundColor(.white)
55 | Text("Sample item 2")
56 | .foregroundColor(.white)
57 | Spacer()
58 | }.padding()
59 | .frame(maxWidth: .infinity, alignment: .leading)
60 | .background(Color.black)
61 | .edgesIgnoringSafeArea(.all)
62 | }
63 | }
64 | }
65 | ```
66 |
67 | ### Installation
68 |
69 | This component is distrubuted as a **Swift package**.
70 |
71 |
--------------------------------------------------------------------------------
/Sources/SwiftUISideMenu/SwiftUISideMenu.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct SideMenu: ViewModifier {
4 | @Binding var isShowing: Bool
5 | private let menuContent: () -> MenuContent
6 |
7 | public init(isShowing: Binding,
8 | @ViewBuilder menuContent: @escaping () -> MenuContent) {
9 | _isShowing = isShowing
10 | self.menuContent = menuContent
11 | }
12 |
13 | public func body(content: Content) -> some View {
14 | let drag = DragGesture().onEnded { event in
15 | if event.location.x < 200 && abs(event.translation.height) < 50 && abs(event.translation.width) > 50 {
16 | withAnimation {
17 | self.isShowing = event.translation.width > 0
18 | }
19 | }
20 | }
21 | return GeometryReader { geometry in
22 | ZStack(alignment: .leading) {
23 | content
24 | .disabled(isShowing)
25 | .frame(width: geometry.size.width, height: geometry.size.height)
26 | .offset(x: self.isShowing ? geometry.size.width / 2 : 0)
27 |
28 | menuContent()
29 | .frame(width: geometry.size.width / 2)
30 | .transition(.move(edge: .leading))
31 | .offset(x: self.isShowing ? 0 : -geometry.size.width / 2)
32 | }.gesture(drag)
33 | }
34 | }
35 | }
36 |
37 | public extension View {
38 | func sideMenu(
39 | isShowing: Binding,
40 | @ViewBuilder menuContent: @escaping () -> MenuContent
41 | ) -> some View {
42 | self.modifier(SideMenu(isShowing: isShowing, menuContent: menuContent))
43 | }
44 | }
45 |
46 | private struct SideMenuTest: View {
47 | @State private var showSideMenu = false
48 |
49 | var body: some View {
50 | NavigationView {
51 | List(1..<6) { index in
52 | Text("Item \(index)")
53 | }
54 | .navigationBarTitle("Dashboard", displayMode: .inline)
55 | .navigationBarItems(leading: (
56 | Button(action: {
57 | withAnimation {
58 | self.showSideMenu.toggle()
59 | }
60 | }) {
61 | Image(systemName: "line.horizontal.3")
62 | .imageScale(.large)
63 | }
64 | ))
65 | }.sideMenu(isShowing: $showSideMenu) {
66 | VStack(alignment: .leading) {
67 | Button(action: {
68 | withAnimation {
69 | self.showSideMenu = false
70 | }
71 | }) {
72 | HStack {
73 | Image(systemName: "xmark")
74 | .foregroundColor(.white)
75 | Text("close menu")
76 | .foregroundColor(.white)
77 | .font(.system(size: 14))
78 | .padding(.leading, 15.0)
79 | }
80 | }.padding(.top, 20)
81 | Divider()
82 | .frame(height: 20)
83 | Text("Sample item 1")
84 | .foregroundColor(.white)
85 | Text("Sample item 2")
86 | .foregroundColor(.white)
87 | Spacer()
88 | }.padding()
89 | .frame(maxWidth: .infinity, alignment: .leading)
90 | .background(Color.black)
91 | .edgesIgnoringSafeArea(.all)
92 | }
93 | }
94 | }
95 |
96 | struct SideMenu_Previews: PreviewProvider {
97 | static var previews: some View {
98 | SideMenuTest()
99 | }
100 | }
101 |
--------------------------------------------------------------------------------