Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
An iOS Developer's Guide to SwiftUI

You're reading from   An iOS Developer's Guide to SwiftUI Design and build beautiful apps quickly and easily with minimum code

Arrow left icon
Product type Paperback
Published in May 2024
Publisher Packt
ISBN-13 9781801813624
Length 446 pages
Edition 1st Edition
Languages
Tools
Concepts
Arrow right icon
Author (1):
Arrow left icon
Michele Fadda Michele Fadda
Author Profile Icon Michele Fadda
Michele Fadda
Arrow right icon
View More author details
Toc

Table of Contents (25) Chapters Close

Preface 1. Part 1: Simple Views FREE CHAPTER
2. Chapter 1: Exploring the Environment – Xcode, Playgrounds, and SwiftUI 3. Chapter 2: Adding Basic UI Elements and Designing Layouts 4. Chapter 3: Adding Interactivity to a SwiftUI View 5. Part 2: Scrollable Views
6. Chapter 4: Iterating Views, Scroll Views, FocusState, Lists, and Scroll View Reader 7. Chapter 5: The Art of Displaying Grids 8. Part 3: SwiftUI Navigation
9. Chapter 6: Tab Bars and Modal View Presentation 10. Chapter 7: All About Navigation 11. Part 4: Graphics and Animation
12. Chapter 8: Creating Custom Graphics 13. Chapter 9: An Introduction to Animations in SwiftUI 14. Part 5: App Architecture
15. Chapter 10: App Architecture and SwiftUI Part I: Practical Tools 16. Chapter 11: App Architecture and SwiftUI Part II – the Theory 17. Part 6: Beyond Basics
18. Chapter 12: Persistence with Core Data 19. Chapter 13: Modern Structured Concurrency 20. Chapter 14: An Introduction to SwiftData 21. Chapter 15: Consuming REST Services in SwiftUI 22. Chapter 16: Exploring the Apple Vision Pro 23. Index 24. Other Books You May Enjoy

Creating custom tab bars

Sometimes, you must implement your take on a tab bar because what you want cannot be created with the standard one; for example, you want something that resembles a tab bar but has custom graphical requirements and is in a different position on the screen. In this section, you will also learn that going for a custom approach requires much more work than the “standard Apple way” of doing things, besides having other drawbacks.

For our customized tab bar, we want it to have round corners, to be drawn in a frame, with a shadow, and to show the title in a different bright color, say in red and in bold, when a tab is selected. We also want it to sit at the top of the user screen rather than at the bottom.

To begin, it is essential to establish the necessary information for presenting a tab, which includes an image representing the non-selected state, an image representing the selected state, and a title. For the sake of simplicity, we will limit the usage to system images and utilize a struct that consists of three String values, as illustrated in the following code snippet:

struct ItemValue {
    let image: String
    let selectedImage: String
    let title: String
}

Then, we are going to need a view capable of displaying our own take on tabItem, in both selected and normal mode. We will call this view MyTabItemView, which will accept two parameters: an ItemValue parameter and the selected Boolean flag.

Depending on the selected flag, the proper system image will be chosen, and the title will be shown either in light or bold, with the selected title also colored in red for good measure.

Create a new SwiftUI view. Go to the Xcode menu and then navigate to File | New | File.... Under iOS, select SwiftUI View under User Interface. In the Save As dialog box, save the file as MyTabItemView.swift. Type the following code as shown:

struct MyTabItemView: View {
    let itemContent: ItemValue
    let selected: Bool
    var body: some View {
        VStack {
            Image(systemName: selected ? itemContent.selectedImage : itemContent.image)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 40, height: 40)
            Spacer().frame(height: 6)
            Text(itemContent.title)
                .fontWeight(selected ? .bold : .light )
                .foregroundColor(selected ? .red : .gray)
                .font(.system(size: 16))
        }
    }
}
struct TabItemView_Previews: PreviewProvider {
    static var previews: some View {
        MyTabItemView(itemContent: ItemValue(image: "moon", selectedImage: "moon.fill", title: "Moon"), selected: true)
    }
}

Then, we are going to need a view to contain all the buttons in our imitation of TabView; let’s call it MyTabView. Create another new SwiftUI View file named MyTabView.swift and add the following code:

import SwiftUI
struct MyTabView: View {
    let items: [ItemValue]
    var height: CGFloat = 72
    var width: CGFloat = UIScreen.main.bounds.width - 28
    @Binding var selectedIndex: Int
    var body: some View {
        HStack {
            Spacer()
            ForEach(items.indices, id: \.self) { index in
                let item = items[index]
                Button {
                    self.selectedIndex = index
                } label: {
                    let isSelected = selectedIndex == index
                    MyTabItemView(itemContent: item, selected: isSelected)
                }
                Spacer()
            }
        }
        .frame(width: width, height: height)
        .background(Color.white)
        .cornerRadius(8)
        .shadow(radius: 3, x: 1, y: 2)
    }
}
struct TabBottomView_Previews: PreviewProvider {
    static var previews: some View {
        MyTabView(
          items: [
            ItemValue(image: "star", selectedImage: "star.fill", title: "Star"),
            ItemValue(image: "triangle", selectedImage: "triangle.fill", title: "Triangle"),
            ItemValue(image: "circle", selectedImage: "circle.fill", title: "Circle"),ItemValue(image: "square", selectedImage: "square.fill", title: "Square")],
          selectedIndex: .constant(0))
    }
}

In the previous code fragment, we keep track of the selected tab using selectedIndex. A tab view knows that it has been selected if its index is the same as selectedIndex. As we want selectedIndex to be accessible externally from this view, we declare it as @Binding.

Also, notice the use of .constant(0) as a way to simulate a value for a binding value in our preview.

Then, we want to use an enum to represent the different values of our tabs, and to be able to use it in a loop, we make it conform to the CaseIterable protocol:

enum Tabs: Int, CaseIterable {
    case moon = 0
    case star
    case triangle
    case square
    var tabItem: ItemValue {
        switch self {
        case .moon:
            return ItemValue(image: "moon", selectedImage: "moon.fill", title: "Moon")
        case .star:
            return ItemValue(image: "star", selectedImage: "star.fill", title: "Star")
        case .triangle:
            return ItemValue(image: "triangle", selectedImage: "triangle.fill", title: "Triangle")
        case .square:
            return ItemValue(image: "square", selectedImage: "square.fill", title: "Square")
        }
    }
}

Furthermore, we need four different views to be selected: one to contain the selection mechanism and the selected view, and one that is that selection mechanism. Let’s call the first CustomTabView and the second HomeTabView. The four views to be selected, in our example, will show their name. In a real app, they can be whatever you want. Our HomeTabView view will cycle through all the values of the enum tabs, using the @ViewBuilder tabView() function to produce its contained views. You will want to use this approach, using @ViewBuilder when you want to produce multiple child views via a single closure or function call. The following code fragment shows how to build all the contained views: StarView, MoonView, TriangleView, and SquareView (they are just Text views with modifiers, each displaying its own name). They all share the same layout with padding, rounded corners, background material, and a shadow.

Create another new SwiftUI View file named HomeTabView.swift and add the following code:

import SwiftUI
struct StarView: View {
    var body: some View {
        Text("This is a Star")
            .padding(.all)
            .background(.regularMaterial)
            .cornerRadius(8.0)
            .shadow(color: .gray,radius: 5,x: 2.0,y: 2)
    }
}
struct MoonView: View {
    var body: some View {
        Text("This is a Moon")
            .padding(.all)
            .cornerRadius(8.0)
            .background(.regularMaterial)
            .shadow(color: .gray,radius: 5,x: 2.0,y: 2)
    }
}
struct TriangleView: View {
    var body: some View {
        Text("This is a Triangle")
            .padding(.all)
            .cornerRadius(8.0)
            .background(.regularMaterial)
            .shadow(color: .gray,radius: 5,x: 2.0,y: 2)
    }
}
struct SquareView: View {
    var body: some View {
        Text("This is a Square")
            .padding(.all)
            .cornerRadius(8.0)
            .background(.regularMaterial)
            .shadow(color: .gray,radius: 5,x: 2.0,y: 2)
    }
}
struct CustomTabView<Content: View>: View {
    let tabs: [ItemValue]
    @Binding var selectedIndex: Int
    @ViewBuilder let content: (Int) -> Content
    var body: some View {
        ZStack {
            TabView(selection: $selectedIndex) {
                ForEach(tabs.indices, id: \.self) { index in
                    content(index)
                        .tag(index)
                }
            }
            VStack {
                MyTabView(items: tabs, selectedIndex: $selectedIndex)
                Spacer()
            }
            .padding(.bottom, 8)
        }
    }
}
struct HomeTabView: View {
    @State var selectedIndex: Int = 0
    var body: some View {
        CustomTabView(tabs: Tabs.allCases.map({ $0.tabItem }), selectedIndex: $selectedIndex) { index in
            let type = Tabs(rawValue: index) ?? .star
            tabView(type: type)
        }
    }
    @ViewBuilder
    func tabView(type: Tabs) -> some View {
        switch type {
        case .star:
            StarView()
        case .moon:
            MoonView()
        case .triangle:
            TriangleView()
        case .square:
            SquareView()
        }
    }
}
struct MainTabView_Previews: PreviewProvider {
    static var previews: some View {
        HomeTabView()
    }
}

The following points explain the preceding code block:

  • CustomTabView is a struct that takes a view (Content: View) as a generic argument. It has the following properties:
    • tabs: An array of ItemValue, which describes each tab
    • selectedIndex: A @State Int that keeps track of the currently selected tab index
    • content: A view builder closure that takes an Int (the index of the selected tab) and returns Content
    • In body, it uses TabView and a custom tab view (MyTabView) in a ZStack to lay out the tabs
  • HomeTabView is the main view of the app. It has a selectedIndex @State variable that tracks the currently selected tab.
  • HomeTabView uses CustomTabView and sets its tabs with Tabs.allCases.map({ $0.tabItem }).
  • Tabs is an enum that describes each tab and has a computed tabItem property of the ItemValue type.
  • The @ViewBuilder func tabView(type: Tabs) -> some View function switches depending on the Tabs enum to determine which of the previously defined views (StarView, MoonView, etc.) is to be displayed.

To finish, we need to add HomeTabView to our default scene, WindowGroup, so that it is the view displayed at the application start. We can delete the ContentView view created by the Xcode wizard, as this is not necessary anymore:

import SwiftUI
@main
struct CH6_CustomTabBarApp: App {
    var body: some Scene {
        WindowGroup {
            HomeTabView()
        }
    }
}

The result is shown in the simulator in the following figure:

Figure 6.4 – Our custom tab bar at the top of the screen

Figure 6.4 – Our custom tab bar at the top of the screen

We can conclude that often customizing existing interface elements just for esthetics can be fun but involves much more work. In our intentionally simple case, this effort was over an order of magnitude higher than just using the native UI elements provided by Apple. Be warned that precisely matching the original component’s capabilities and functionality is also difficult. In our case, we sacrificed compatibility and ease of use with Apple TV and future Apple devices to display our titles in red and position the tab bar at the top of the screen in iOS.

Users may already be familiar with the existing UI elements unless using your customized versions is immediately understandable by a user. So, whenever you decide to implement your own modified version of an existing UI element, you should question yourself and consider whether that time would be better spent producing more value for your user by implementing more actual features.

Besides, if your customization effort goes too deep, it might be based on some mechanism that Apple will want to change in the next versions of iOS.

If you stick to Apple UI recommendations, if Apple decides to change the look and feel of apps in the next version of the operating system, your app will adapt, often without any modifications required on your side.

When this does not happen automatically, you may want to let your app perform differently under different conditions – namely, different versions of the compiler (which practically means different versions of Xcode) or different versions of the iOS operating system. In the next section, we will teach you how to adapt source code to different versions of the OS at compile time.

Adapting your code to different versions of the operating system

Conditional compilation rather than runtime check in Swift

If you want to adapt your code so that it is produced only for a certain version of the compiler or operating system, you can use conditional compilation. In the following example, different versions of the code will be compiled, depending on the version of Swift.

You can also check for a specific version of an operating system, both at compile time and at runtime. If you don’t want to link to the wrong version of an API, be sure to use conditional compilation.

An example of conditional compilation depending on the compiler is the following code fragment:

#if compiler(>=5.5)
    return self.previewCGImageRepresentation()
    #else
      return   self.previewCGImageRepresentation()?.takeUnretainedValue()
#endif

The previous code fragment provides a different implementation of the preview image representation, depending on the compiler version. The inappropriate version would not be even linked, and this will avoid producing an error if the compiler version does not support that functionality.

If, instead, you want to compile different code depending on the supported API for different versions of the operating systems, you can use if #available, as in the following code fragment:

struct ContentView: View {
     var body: some View {
     Group {
            if #available(iOS 14.0, *) {
                ScrollView {
                    AnyView(LazyVStack { content.padding(.horizontal, 15) })
                }
            } else {
                List { content }
            }
        }
    }
}

In the previous code example, ContentView will contain a Group-based view hierarchy or a simple List, depending on the version of iOS being equal to or above 14.0.

In the next section, we will examine modal views in detail.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image