Last active
February 10, 2021 15:32
-
-
Save hfossli/f93cb5d1fdbe311b5a1bcbdd2a04cf0f to your computer and use it in GitHub Desktop.
SimpleTabView
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} | |
} | |
} | |
} |
Here's an article on medium about this topic https://hfossli.medium.com/lifecycle-bugs-in-swiftuis-tabview-c130b79d1cb
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example