Custom Menu/Tab bar in SwiftUI - swift

I want to make tab bar like this and do not know where to start. Please help me I'm a newbie!
It navigates to different views.

Here's a pretty functional version. This could be made better to further mirror SwiftUI's TabBar interface. Let me know if you run into any issues with this.
(full res)
protocol TopTab: RawRepresentable, Identifiable, CaseIterable, Hashable where AllCases == Array<Self> {
var title: String { get }
}
struct TabBackground: Shape {
var cornerRadius: CGFloat = 8
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: .zero + cornerRadius, y: .zero))
path.addLine(to: CGPoint(x: rect.width - cornerRadius, y: .zero))
path.addQuadCurve(to: CGPoint(x: rect.width, y: .zero + cornerRadius), control: CGPoint(x: rect.width, y: .zero))
path.addLine(to: CGPoint(x: rect.width, y: rect.height - cornerRadius))
path.addQuadCurve(to: CGPoint(x: rect.width + cornerRadius, y: rect.height), control: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: .zero - cornerRadius, y: rect.height))
path.addQuadCurve(to: CGPoint(x: .zero, y: rect.height - cornerRadius), control: CGPoint(x: .zero, y: rect.height))
path.addLine(to: CGPoint(x: .zero, y: cornerRadius))
path.addQuadCurve(to: CGPoint(x: .zero + cornerRadius, y: .zero), control: .zero)
return path
}
}
struct TopScrollingTabBarPicker<Tab: TopTab>: View {
#Binding var selection: Tab
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(Tab.allCases) { tab in
Group {
if tab == selection {
Text(tab.title)
.bold()
.foregroundColor(.accentColor)
.padding(12)
.background {
TabBackground(cornerRadius: 16)
.fill(Color.white)
}
} else {
Text(tab.title)
.padding(4)
.foregroundColor(.white)
}
}
.tag(tab)
.onTapGesture {
selection = tab
}
}
}
.padding(.horizontal, 10)
}
.background(Color.accentColor)
}
}
struct TopScrollingTabBar<Tab: TopTab, Content: View>: View {
var alignment: HorizontalAlignment = .leading
#Binding var selection: Tab
#ViewBuilder var content: (_ tab: Tab) -> Content
var body: some View {
GeometryReader { geometry in
VStack(alignment: alignment) {
TopScrollingTabBarPicker(selection: $selection)
.frame(height: 44)
ZStack {
Color(.systemBackground)
.ignoresSafeArea()
self.content(selection)
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
}
}
}
}
}
Usage:
enum PreviewTab: String, TopTab {
case saved
case booked
case pending
case confirmed
case archived
var id: String {
rawValue
}
var title: String {
switch self {
case .saved:
return "Saved"
case .booked:
return "Booked"
case .pending:
return "Pending"
case .confirmed:
return "Confirmed"
case .archived:
return "Archived"
}
}
}
struct ContentView: View {
#State var selection: PreviewTab = .saved
var body: some View {
TopScrollingTabBar(selection: $selection.animation()) { tab in
// For demo purposes
List {
Text(tab.title)
.bold()
.font(.largeTitle)
Text("Something that is \(tab.title)")
}
.listStyle(.plain)
// In a full app I'd switch on the Tab enum and generate the appropriate view.
// switch tab {
// case .saved:
// SavedThings()
// case .booked:
// ...etc
// }
}
}
}
Hope this helps! <3

Related

How to implement a slider whose minimum track color is begin from center(value=0) not left in SwiftUI

I want to custom Slider in SwiftUI. Just something like this.
I tried Slider with GeometryReader but it isn't working.
//MARK: Left - Right Balance
GeometryReader { geo in
VStack {
Text("\(String(format: "%.2f", balanceVolume))")
HStack {
Text("L")
Slider(value: $balanceVolume, in: minValue...maxValue, step: 0.1) {editing in
print("editing", editing)
isEditing = editing
if !editing {
player.pan = Float(balanceVolume)
}
}
.tint(.none)
.accentColor(.gray)
Text("R")
}
}
.padding(20)
}
Thank you you all.
I have created a simple custom slider, I hope it helps
Output:
Use:
struct slider: View {
#State var sliderPosition: Float = 50
var body: some View {
SliderView(value: $sliderPosition, bounds: 1...100).padding(.all)
}
}
Code:
struct SliderView: View {
let currentValue: Binding<Float>
let sliderBounds: ClosedRange<Int>
public init(value: Binding<Float>, bounds: ClosedRange<Int>) {
self.currentValue = value
self.sliderBounds = bounds
}
var body: some View {
GeometryReader { geomentry in
sliderView(sliderSize: geomentry.size)
}
}
#ViewBuilder private func sliderView(sliderSize: CGSize) -> some View {
let sliderViewYCenter = sliderSize.height / 2
let sliderViewXCenter = sliderSize.width / 2
ZStack {
RoundedRectangle(cornerRadius: 2)
.fill(Color.gray)
.frame(height: 3)
ZStack {
let sliderBoundDifference = sliderBounds.count
let stepWidthInPixel = CGFloat(sliderSize.width) / CGFloat(sliderBoundDifference)
let thumbLocation = CGFloat(currentValue.wrappedValue) * stepWidthInPixel
// Path between starting point to thumb
lineBetweenThumbs(from: .init(x: sliderViewXCenter, y: sliderViewYCenter), to: .init(x: thumbLocation, y: sliderViewYCenter))
// Thumb Handle
let thumbPoint = CGPoint(x: thumbLocation, y: sliderViewYCenter)
thumbView(position: thumbPoint, value: Float(currentValue.wrappedValue))
.highPriorityGesture(DragGesture().onChanged { dragValue in
let dragLocation = dragValue.location
let xThumbOffset = min(dragLocation.x, sliderSize.width)
let newValue = Float(sliderBounds.lowerBound / sliderBounds.upperBound) + Float(xThumbOffset / stepWidthInPixel)
if newValue > Float(sliderBounds.lowerBound) && newValue < Float(sliderBounds.upperBound + 1) {
currentValue.wrappedValue = newValue
}
})
}
}
}
#ViewBuilder func lineBetweenThumbs(from: CGPoint, to: CGPoint) -> some View {
Path { path in
path.move(to: from)
path.addLine(to: to)
}.stroke(Color.blue, lineWidth: 4)
}
#ViewBuilder func thumbView(position: CGPoint, value: Float) -> some View {
ZStack {
Text(String(round(value)))
.font(.headline)
.offset(y: -20)
Circle()
.frame(width: 24, height: 24)
.foregroundColor(.accentColor)
.shadow(color: Color.black.opacity(0.16), radius: 8, x: 0, y: 2)
.contentShape(Rectangle())
}
.position(x: position.x, y: position.y)
}
}

SwiftUI animate opacity in gradient mask

I want to animate a mountain line chart (see screenshot) so that the line appears smoothly from left to right. I tried animating the endPoint of a LinearGradient in a mask. This works as it should, but leaves the right end of the line with an opacity value < 1. I also tried animating the opacity value itself, but that doesn't animate anything. This is the View:
import SwiftUI
struct ContentView: View {
private var datapoints: [CGFloat] = [100, 250, 200, 220, 200, 250, 350, 300, 325, 250]
#State private var pct: Double = 0.0
var body: some View {
GeometryReader { geometry in
ZStack {
Path() { path in
var x = 0.0
path.move(to: CGPoint(x: x, y: datapoints[0]))
for i in (1 ..< datapoints.count) {
x += geometry.size.width / CGFloat(datapoints.count - 1)
path.addLine(to: CGPoint(x: x, y: datapoints[i]))
}
}
.stroke()
Path() { path in
var x: Double = 0.0
path.move(to: CGPoint(x: x, y: datapoints[0]))
for i in (1 ..< datapoints.count) {
x += geometry.size.width / CGFloat(datapoints.count - 1)
path.addLine(to: CGPoint(x: x, y: datapoints[i]))
}
path.addLine(to: CGPoint(x: path.currentPoint!.x, y: geometry.size.height))
path.addLine(to: CGPoint(x: 0, y: geometry.size.height))
}
.fill(LinearGradient(colors: [.black.opacity(0.25), .black.opacity(0)], startPoint: .top, endPoint: .bottom))
}
.mask(LinearGradient(stops: [Gradient.Stop(color: .black, location: 0), Gradient.Stop(color: .black.opacity(pct), location: 1)], startPoint: .leading, endPoint: .trailing))
.onAppear() {
withAnimation(.linear(duration: 3)) {
pct = 1.0
}
}
// .mask(LinearGradient(stops: [Gradient.Stop(color: .black, location: 0), Gradient.Stop(color: .black.opacity(0), location: 1)], startPoint: .leading, endPoint: pct == 1 ? .trailing : .leading))
// .onAppear() {
// withAnimation(.linear(duration: 3)) {
// pct = 1.0
// }
// }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is the result if uncomment the commented out lines:

How can I clip a Shape with another Shape in SwiftUI?

I want to clip the red square with the green half-circle. I tried .mask and .clipShape but
i can't get this to work. Can someone share how its done please?
import SwiftUI
struct TestEndBarView: View {
var body: some View {
ZStack {
Rectangle()
.fill(Color.red)
.frame(width: 500, height: 500)
Circle()
.trim(from: 0.5, to: 1.0)
.fill(Color.green)
.rotationEffect(Angle(degrees: 90))
.frame(width: 500, height: 500)
.offset(x: -250)
}
}
}
struct TestEndBarView_Previews: PreviewProvider {
static var previews: some View {
TestEndBarView()
}
}
with using eoFill like in the code:
struct ContentView: View {
var body: some View {
CustomShape()
.fill(Color.purple, style: FillStyle(eoFill: true, antialiased: true))
.frame(width: 200, height: 200, alignment: .center)
.shadow(radius: 10.0)
}
}
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.addArc(center: CGPoint(x: rect.minX, y: rect.midY), radius: rect.height/2, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 270), clockwise: true)
path.addRect(CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: rect.height))
}
}
}
Create a struct that conforms to Shape.
Implement func path(in rect: CGRect) -> Path
Create a Path. Add the rectangle, then the circle.
Fill the shape with isEOFilled set to true
My code is this basic one. Maybe someone finds this useful so i am attaching it here as well.
import SwiftUI
struct TestEndBarView: View {
var body: some View {
ZStack {
BoxCutOut()
.foregroundColor(Color.red)
}
}
}
struct TestEndBarView_Previews: PreviewProvider {
static var previews: some View {
TestEndBarView()
}
}
struct BoxCutOut: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x:0, y: 0))
path.addLine(to: CGPoint(x:rect.width, y:0))
path.addLine(to: CGPoint(x:rect.width, y:rect.height))
path.addLine(to: CGPoint(x:0, y:rect.height))
path.addArc(center: CGPoint(x: 0, y: rect.height/2), radius: rect.height/2, startAngle: .degrees(90), endAngle: .degrees(270), clockwise: true )
}
}
}

How do I show a new view after some delay amount of time SwiftUI

So I am building a blackjack simulator game and my problem right now is making it more realistic. From when I press the "Hit Me" button I'd like a delay before my next card is uploaded. Similarily when the next player goes I'd also like a delay. So right now I have aplayer view a hand view and a card view. The player and hand are both observables.
var body: some View {
HStack {
VStack(alignment: .trailing, spacing: 0){
ForEach(0..<self.player.hands.count, id: \.self) {
index in ZStack {
Spacer()
HandView.init(hand: self.player.hands[index])
}
}
Spacer().frame(height: 45)
Text(self.player.id).bold().font(Font.system(size: 20))
Spacer()
}
.padding(.all)
if !player.isRobot{
VStack{Button(action: {
self.player.requestCard()
}, label: {
Text("Hit Me!")
})
Button(action: {
self.player.handleInput(turn: turnPosibilities.stay)
self.player.objectWillChange.send()
}, label: {
Text("Stay")
})}
.offset(x: 10, y: 0)}
}
}
#ObservedObject var hand:Hand
var bust: some View {
GeometryReader { geometry in
Path { path in
path.move(to: CGPoint.init(x: geometry.frame(in: .local).midX, y: CGFloat(geometry.frame(in: .local).midY)))
path.addLine(to: CGPoint.init(x: geometry.frame(in: .local).minX, y: geometry.frame(in: .local).minY))
path.addLine(to: CGPoint.init(x: geometry.frame(in: .local).minX, y: geometry.frame(in: .local).maxY))
path.addLine(to: CGPoint.init(x: geometry.frame(in: .local).maxX, y: geometry.frame(in: .local).maxY))
path.addLine(to: CGPoint.init(x: geometry.frame(in: .local).maxX, y: geometry.frame(in: .local).minY))
}
.fill(Color.red)
}
}
var body: some View {
ZStack
{ForEach(0..<self.hand.cards.count, id: \.self){
card in
VStack(alignment: .leading, spacing: 2) {PlayingCardView(rank: self.hand.cards[card].rankRaw, suit: self.hand.cards[card].suit, isFaceUp: self.hand.cards[card].isFaceUp ).rotationEffect(Angle.init(degrees: Double(multiply(index: card, offset: 10))))
.offset(x: multiply(index: card, offset: 15), y: multiply(index: card, offset: 40))
}
}
}
}
}
ZStack{
if isFaceUp {
Rectangle().frame(width: 130, height:182).foregroundColor(Color.init(#colorLiteral(red: 0.9999127984, green: 1, blue: 0.9998814464, alpha: 1))).cornerRadius(25).overlay(Image(self.suit.rawValue).resizable().aspectRatio(contentMode: .fit).padding(.all))
Text(String(self.rank)).font(.custom("Poppins-SemiBoldItalic", size: 40)).offset(x: 29, y: -70)
Text(String(self.rank)).font(.custom("Poppins-SemiBoldItalic", size: 40)).offset(x: 29, y: -70).rotationEffect(Angle.init(degrees: 180))
}
else {
Rectangle().frame(width: 130, height:182).foregroundColor(Color.init(UIColor(red: 0.58, green: 0.65, blue: 0.65, alpha: 1.00)
)).cornerRadius(25)
}
}.clipShape(RoundedRectangle(cornerRadius: 25)).overlay(RoundedRectangle(cornerRadius: 25).stroke(Color.black, lineWidth: 2))
}
Here is possible approach
VStack{Button(action: {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // 1 sec delay
self.player.requestCard()
}
}, label: {
Text("Hit Me!")
})
This can be achieved in SwiftUI by defining a #State variable / flag and showing new view based on that. In the below code snippet showSecondView determines when to show the next view.
struct ContentView: View {
#State var showSecondView = false
var body: some View {
Group {
Text("Hello, World!")
.font(.largeTitle)
.foregroundColor(.primary)
if showSecondView {
Text("SecondView")
.font(.title)
.foregroundColor(.secondary)
}
}
.onAppear() {
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { (_) in
withAnimation {
self.showSecondView = true
}
}
}
}

Multidevice layout SwiftUI

I have this layout working fine in iPhone 11, but when I switch to 8, the top background color gets disproportionate, how can I make it have the same height? Is there such a thing as height %?
and the one that gets weird:
This is the code:
import SwiftUI
struct ContentView: View {
var body: some View {
TabView {
HomeView()
.tabItem {
VStack {
Image(systemName: "1.circle")
Text("Home")
}
}.tag(1)
}
}
}
struct HomeView: View {
var body: some View {
ZStack {
SetBackground()
Text("Home View")
.font(.largeTitle)
}
}
}
struct ArcShape : Shape {
let geometry: GeometryProxy
func path(in rect: CGRect) -> Path {
var p = Path()
let center = CGPoint(x: 290, y: 100)
p.addArc(center: center, radius: geometry.size.width * 3, startAngle: .degrees(39), endAngle: .degrees(140), clockwise: false)
return p
}
}
struct SetBackground: View {
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Color.white
.edgesIgnoringSafeArea(.all)
ArcShape(geometry: geometry)
.offset(x: geometry.size.width * -0.3, y: geometry.size.height * -1.49)
.foregroundColor(.yellow)
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
}
}
}
The rest is working fine, it's just that top background that shifts if the iPhone model changes. I tried putting it in a frame but that just gets it weird on the middle :/
Also, why does it changes if I put the tabView and only leave HomeView()? It's so weird
Thanks
It is due to hardcoded Arc center & different safe-areas on different devices.
So the solution is to make Arc position geometry dependent (specific factors you can fit)
struct ArcShape : Shape {
let geometry: GeometryProxy
func path(in rect: CGRect) -> Path {
var p = Path()
let center = CGPoint(x: geometry.size.width / 2, y: -geometry.size.height * 1.75) // 1/4 from top
p.addArc(center: center, radius: geometry.size.height * 2, startAngle: .degrees(39), endAngle: .degrees(140), clockwise: false)
return p
}
}
struct SetBackground: View {
var body: some View {
NavigationView {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Color.white
ArcShape(geometry: geometry)
.foregroundColor(.yellow)
}
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarHidden(true)
}
}
}