Skip to content

Instantly share code, notes, and snippets.

@hfossli
Last active February 10, 2021 15:32
Show Gist options
  • Save hfossli/f93cb5d1fdbe311b5a1bcbdd2a04cf0f to your computer and use it in GitHub Desktop.
Save hfossli/f93cb5d1fdbe311b5a1bcbdd2a04cf0f to your computer and use it in GitHub Desktop.
SimpleTabView
import Foundation
import SwiftUI
struct SimpleTab<SelectionValue: Equatable & Hashable>: View, Identifiable {
var id: SelectionValue
var image: Image
var label: Text
var content: AnyView
init<Content: View>(id: SelectionValue,
@ViewBuilder icon: (SelectionValue) -> TupleView<(Image, Text)>,
@ViewBuilder content: (SelectionValue) -> Content) {
let tabIcon = icon(id)
self.id = id
self.image = tabIcon.value.0
self.label = tabIcon.value.1
self.content = AnyView(content(id))
}
var body: some View {
return self.content
}
}
struct SimpleTabView<SelectionValue: Hashable & Equatable>: View {
/*
SwiftUI's TabView is riddled with lifecycle bugs:
1. It creates view trees for tabs which is not visible
2. It yields an extra onAppear, onDisappear and onAppear when making a tab visible
3. It yields an extra onApepar callback when the whole tab is removed
*/
@Binding var selection: SelectionValue
var tabs: [SimpleTab<SelectionValue>]
@State var shown: [SelectionValue] = []
@ViewBuilder
var body: some View {
GeometryReader { proxy in
ZStack(alignment: .center) {
VStack(alignment: .center, spacing: 0) {
HStack(alignment: .center, spacing: 0) {
Spacer(minLength: 0)
VStack(alignment: .center, spacing: 0) {
Spacer(minLength: 0)
ZStack(alignment: .center) {
ForEach(tabs) { tab in
if shown.contains(tab.id) || selection == tab.id {
tab.content
.hide(selection != tab.id).id(tab.id)
.onAppear {
if !self.shown.contains(tab.id) {
self.shown.append(tab.id)
}
}
}
}
}
Spacer(minLength: 0)
}
Spacer(minLength: 0)
}
VStack(alignment: .center, spacing: 0) {
Rectangle()
.frame(height: 0.5)
.foregroundColor(Color.black.opacity(0.2))
HStack(alignment: .center, spacing: 0) {
ForEach(tabs) { tab in
Button(action: {
self.selection = tab.id
}, label: {
VStack(alignment: .center, spacing: 0) {
tab.image
.resizable()
.aspectRatio(contentMode: .fit)
tab.label
.padding(.top, 4)
}
.frame(width: itemWidth(for: proxy.size.width))
.frame(height: 44)
.padding(.vertical, 8)
})
.foregroundColor(foregroundColorForTabId(tab.id))
}
}
.padding(.top, 8)
.frame(height: 64)
.padding(.bottom, proxy.safeAreaInsets.bottom)
}
.background(Blur(style: .systemMaterial))
}
.edgesIgnoringSafeArea(.bottom)
}
}
}
func itemWidth(for viewWidth: CGFloat) -> CGFloat {
return (viewWidth / CGFloat(tabs.count))
}
func foregroundColorForTabId(_ id: SelectionValue) -> Color {
return id == selection ? .black : Color.black.opacity(0.3)
}
struct Blur: UIViewRepresentable {
var style: UIBlurEffect.Style = .systemMaterial
func makeUIView(context: Context) -> UIVisualEffectView {
return UIVisualEffectView(effect: UIBlurEffect(style: style))
}
func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}
}
import SwiftUI
import UIKit
extension View {
func hide(_ hide: Bool) -> some View {
HideableView(isHidden: .constant(hide), view: self)
}
func hide(_ isHidden: Binding<Bool>) -> some View {
HideableView(isHidden: isHidden, view: self)
}
}
struct HideableView<Content: View>: UIViewRepresentable {
@Binding var isHidden: Bool
var view: Content
func makeUIView(context: Context) -> ViewContainer<Content> {
return ViewContainer(isContentHidden: isHidden, child: view)
}
func updateUIView(_ container: ViewContainer<Content>, context: Context) {
container.child.rootView = view
container.isContentHidden = isHidden
}
class ViewContainer<Content: View>: UIView {
var child: UIHostingController<Content>
var didShow = false
var isContentHidden: Bool {
didSet {
addOrRemove()
}
}
init(isContentHidden: Bool, child: Content) {
self.child = UIHostingController(rootView: child)
self.isContentHidden = isContentHidden
super.init(frame: .zero)
addOrRemove()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
child.view.frame = bounds
}
func addOrRemove() {
if isContentHidden && child.view.superview != nil {
child.view.removeFromSuperview()
}
if !isContentHidden && child.view.superview == nil {
if !didShow {
DispatchQueue.main.async {
if !self.isContentHidden {
self.addSubview(self.child.view)
self.didShow = true
}
}
} else {
addSubview(child.view)
}
}
}
}
}
@hfossli
Copy link
Author

hfossli commented Oct 22, 2020

Example

enum MyTabs: String {
    case health
    case settings
    
    var title: String {
        return rawValue.prefix(1).capitalized + rawValue.dropFirst()
    }
}

struct MyView: View {
    var title: String
    var body: some View {
        NavigationView {
            Text("Empty").navigationTitle(title)
        }
        .onAppear {
            print("\(title) onAppear")
        }
        .onDisappear {
            print("\(title) onDisappear")
        }
    }
}

struct ContentView: View {
    @State var selectedTab = MyTabs.health
        
    var body: some View {
        SimpleTabView(
            selection: $selectedTab,
            tabs: [
                
                SimpleTab(id: .health, icon: { id in
                    Image(systemName: "heart")
                    Text(id.title)
                }, content: { id in
                    MyView(title: id.title)
                }),
                
                SimpleTab(id: .settings, icon: { id in
                    Image(systemName: "gear")
                    Text(id.title)
                }, content: { id in
                    MyView(title: id.title)
                })
                
            ]
        )
    }
}

@hfossli-agens
Copy link

hfossli-agens commented Oct 22, 2020

@hfossli
Copy link
Author

hfossli commented Oct 22, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment