How to make SwiftUI Buttons a fixed size? - swift

I am trying to create 2 buttons of equal size, but the size is determined by the text inside the button. The two buttons are "login" and "create account", so the "create account" button is larger. I tried to use .frame() to adjust the size, but that just made it have extra padding around it.
Here is my code:
HStack {
Button(action: {
print("Login button pressed")
}) {
Text("Login")
.padding()
.foregroundColor(Color.white)
.background(Color(UIColor.buttonColor))
.cornerRadius(15)
}
Button(action: {
print("Create Account button pressed")
}) {
Text("Create Account")
.padding()
.foregroundColor(Color.white)
.background(Color(UIColor.buttonColor))
.cornerRadius(15)
}
}
And here is what that displays

Best to make a button style you can use across your app which will also ensure any future stack is proportionally filled. Otherwise you'll find your UIs will get messy making Buttons with Text()s like that.
Laying out the buttons with a custom Style in the HStack:
HStack(spacing: 20) {
Button("Login", action: { print("login")})
.buttonStyle(PrimaryButtonStyle())
Button("Create Account", action: { print("create account")})
.buttonStyle(PrimaryButtonStyle())
}.padding([.leading, .trailing], 10)
PrimaryButtonStyle:
struct PrimaryButtonStyle: ButtonStyle {
var backgroundColor: Color = .black
var textColor: Color = Color.white
var height: CGFloat = 46
var cornerRadius: CGFloat = 15
var fontSize: CGFloat = 15
var disabled: Bool = false
var textSidePadding: CGFloat = 30
var weight: Font.Weight = .semibold
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding([.leading, .trailing], textSidePadding)
.frame(maxWidth: .infinity, maxHeight: height)
.background(disabled ? .gray : backgroundColor)
.foregroundColor(textColor)
.cornerRadius(cornerRadius)
.font(.system(size: fontSize, weight: weight, design: .default))
.scaleEffect(configuration.isPressed ? 1.2 : 1)
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
}
}
Result:
:

.frame and set width to UI screen - blank should work

Related

How can I make a smoothed custom Picker on SwiftUI?

I would like to replicate this picker in swiftUI. In particular, I have a button on the bottom left of the screen and when I click it I would like to show different icons (similar to the image below, but vertically). As soon as I click on one of the choices the button should shrink back to the initial form (circle) with the chosen icon.
When closed:
When open:
I am new to this language and to app in general, I tried with a Pop Up menu, but it is not the desired result, for now I have an horizontal segmented Picker.
You can't do this with the built-in Picker, because it doesn't offer a style like that and PickerStyle doesn't let you create custom styles (as of the 2022 releases).
You can create your own implementation out of other SwiftUI views instead. Here's what my brief attempt looks like:
Here's the code:
enum SoundOption {
case none
case alertsOnly
case all
}
struct SoundOptionPicker: View {
#Binding var option: SoundOption
#State private var isExpanded = false
var body: some View {
HStack(spacing: 0) {
button(for: .none, label: "volume.slash")
.foregroundColor(.red)
button(for: .alertsOnly, label: "speaker.badge.exclamationmark")
.foregroundColor(.white)
button(for: .all, label: "volume.2")
.foregroundColor(.white)
}
.buttonStyle(.plain)
.background {
Capsule(style: .continuous).foregroundColor(.black)
}
}
#ViewBuilder
private func button(for option: SoundOption, label: String) -> some View {
Button {
withAnimation(.easeOut) {
if isExpanded {
self.option = option
isExpanded = false
} else {
isExpanded = true
}
}
} label: {
Image(systemName: label)
.fontWeight(.bold)
.padding(10)
}
.frame(width: shouldShow(option) ? buttonSize : 0, height: buttonSize)
.opacity(shouldShow(option) ? 1 : 0)
.clipped()
}
private var buttonSize: CGFloat { 44 }
private func shouldShow(_ option: SoundOption) -> Bool {
return isExpanded || option == self.option
}
}
struct ContentView: View {
#State var option = SoundOption.none
var body: some View {
ZStack {
Color(hue: 0.6, saturation: 1, brightness: 0.2)
SoundOptionPicker(option: $option)
.shadow(color: .gray, radius: 3)
.frame(width: 200, alignment: .trailing)
}
}
}

Add shadow above SwiftUI's TabView

I'm trying to implement TabView in SwiftUI that has the same color as screen background but also has a shadow above it like in this picture:
So far I've been able to properly display color, but I don't know how to add the shadow. This is my code:
struct ContentView: View {
init() {
let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
UITabBar.appearance().standardAppearance = appearance
}
var body: some View {
TabView {
Text("First View")
.tabItem {
Image(systemName: "square.and.arrow.down")
Text("First")
}
Text("Second View")
.tabItem {
Image(systemName: "square.and.arrow.up")
Text("Second")
}
}
}
}
Does anyone know how to do this? I would appreciate your help :)
Short answer
I found a solution. You can create your own shadow image and add it to the UITabBar appearance like this:
// load your custom shadow image
let shadowImage: UIImage = ...
//you also need to set backgroundImage, without it shadowImage is ignored
UITabBar.appearance().backgroundImage = UIImage()
UITabBar.appearance().shadowImage = shadowImage
More detailed answer
Setting backgroundImage
Note that by setting
UITabBar.appearance().backgroundImage = UIImage()
you make your TabView transparent, so it is not ideal if you have content that can scroll below it. To overcome this, you can set TabView's color.
let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
appearance.backgroundColor = UIColor.systemGray6
UITabBar.appearance().standardAppearance = appearance
Setting shadowImage
I wanted to generate shadow image programatically. For that I've created an extension of UIImage. (code taken from here)
extension UIImage {
static func gradientImageWithBounds(bounds: CGRect, colors: [CGColor]) -> UIImage {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colors
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
And finally I styled my TabView like this:
let image = UIImage.gradientImageWithBounds(
bounds: CGRect( x: 0, y: 0, width: UIScreen.main.scale, height: 8),
colors: [
UIColor.clear.cgColor,
UIColor.black.withAlphaComponent(0.1).cgColor
]
)
let appearance = UITabBarAppearance()
appearance.configureWithTransparentBackground()
appearance.backgroundColor = UIColor.systemGray6
appearance.backgroundImage = UIImage()
appearance.shadowImage = image
UITabBar.appearance().standardAppearance = appearance
Result
Your best bet in order to achieve pretty much exactly what you wish is to create a custom TabView.
In fact, in SwiftUI you could use UITabBarAppearance().shadowColor, but that won't do much apart from drawing a 2px line on top of the TabView itself.
Instead, with the below code, you could create a custom TabView and achieve the desired graphical effect.
import SwiftUI
enum Tab {
case borrow,ret,device,you
}
struct TabView: View {
#Binding var tabIdx: Tab
var body: some View {
HStack {
Group {
Spacer()
Button (action: {
self.tabIdx = .borrow
}) {
VStack{
Image(systemName: "arrow.down.circle")
Text("Borrow")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .borrow ? .purple : .secondary)
Spacer()
Button (action: {
self.tabIdx = .ret
}) {
VStack{
Image(systemName: "arrow.up.circle")
Text("Return")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .ret ? .purple : .secondary)
Spacer()
Button (action: {
self.tabIdx = .device
}) {
VStack{
Image(systemName: "safari")
Text("Device")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .device ? .purple : .secondary)
Spacer()
Button (action: {
self.tabIdx = .you
}) {
VStack{
Image(systemName: "person.circle")
Text("You")
.font(.system(size: 10))
}
}
.foregroundColor(self.tabIdx == .you ? .purple : .secondary)
Spacer()
}
}
.padding(.bottom, 30)
.padding(.top, 10)
.background(Color(red: 0.95, green: 0.95, blue: 0.95))
.font(.system(size: 30))
.frame(height: 80)
}
}
struct ContentView: View {
#State var tabIdx: Tab = .borrow
var body: some View {
NavigationView {
VStack(spacing: 20) {
Spacer()
if tabIdx == .borrow {
Text("Borrow")
} else if tabIdx == .ret {
Text("Return")
} else if tabIdx == .device {
Text("Device")
} else if tabIdx == .you {
Text("You")
}
Spacer(minLength: 0)
TabView(tabIdx: self.$tabIdx)
.shadow(radius: 10)
}
.ignoresSafeArea()
}
}
}
Remember that when you do this, all your tabs are specified as cases within enum Tab {}, and the TabView() contains some Button elements, which will change the tab by using the #State var tabIdx. So you can adjust your actions by modifying the self.tabIdx = <yourtab> statement.
The active color is set with this statement after each button:
.foregroundColor(self.tabIdx == .borrow ? .purple : .secondary)
Here you can just change .purple to whatever suits you.
You can see that on the ContentView, the if-else if block catches the SubView. Here I have placed some Text() elements like Text("Borrow"), so you can replace them by calling something like BorrowView() or whatever you have.
I have added the shadow when I call the TabView from within the ContentView, but you could even add the .shadow(radius: 10) after the HStack of the TabView itself.
This would be the final output:
                 

Spacer Breaks Button Appearance, Button Still Toggles

I have created an HStack that houses a collection of buttons that change appearance when selected and deselected. Only one button can be selected at a time. When adding a Spacer() between items, the button toggle works, however the appearance no longer changes, even though the index selected is changing as it's supposed to.
struct GenericFilterButton: View {
var category: String
var textColor: Color
var buttonColor: Color
var body: some View {
Text(category)
.font(.custom("Avenir Heavy", size: 14))
.foregroundColor(textColor)
.padding(.vertical, 10)
.padding(.horizontal, 14)
.background(buttonColor)
.cornerRadius(50)
.lineLimit(1)
}
}
struct FilterViewCompletionPercent: View {
var completionPercent = ["Any", "25%", "50%", "75%", "100%"]
#State var percentSelected = 0
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text("Completion")
.font(.custom("Avenir Heavy", size: 18))
.foregroundColor(Color.black.opacity(0.5))
.padding(.bottom, 20)
HStack(alignment: .center) {
ForEach(0..<completionPercent.count) { i in
Button(action: {
self.percentSelected = i
print(self.percentSelected)
}) {
GenericFilterButton(category: self.completionPercent[i], textColor: self.percentSelected == i ? Color.white : Color.black.opacity(0.5), buttonColor: self.percentSelected == i ? Color.blue : Color.white.opacity(0.0))
}
if i != 4 {
Spacer()
}
}
}.padding(.bottom, 20)
Divider()
.background(Color.black.opacity(0.1))
}.padding(.horizontal, 25)
.padding(.top, 30)
}
}
Why may this be happening? I can't seem to think of a plausible reason why adding a Spacer() between items doesn't work, but when adding a Spacer() without the if statement does.
Dynamic content of ForEach should be single view, so embed your Button-Spacer pair into another HStack.
Tested with Xcode 11.4 / iOS 13.4
HStack(alignment: .center) {
ForEach(0..<completionPercent.count, id: \.self) { i in
HStack { // << this one !!
Button(action: {
self.percentSelected = i
print(self.percentSelected)
}) {
GenericFilterButton(category: self.completionPercent[i], textColor: self.percentSelected == i ? Color.white : Color.black.opacity(0.5), buttonColor: self.percentSelected == i ? Color.blue : Color.white.opacity(0.0))
}
if i != 4 {
Spacer()
}
}
}
}.padding(.bottom, 20)

SwiftUI Segmented Picker does not switch when click on it

I'm trying to implement Segmented Control with SwiftUI. For some reason Segmented Picker does not switch between values when click on it. I went through many tutorials but cannot find any difference from my code:
struct MeetingLogin: View {
#State private var selectorIndex: Int = 0
init() {
UISegmentedControl.appearance().backgroundColor = UIColor(named: "JobsLightGreen")
UISegmentedControl.appearance().selectedSegmentTintColor = UIColor(named: "AlmostBlack")
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white,
.font: UIFont(name: "OpenSans-Bold", size: 13)!],
for: .selected)
UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white,
.font: UIFont(name: "OpenSans-Regular", size: 13)!],
for: .normal)
}
var body: some View {
VStack {
...
Group {
Spacer().frame(minHeight: 8, maxHeight: 30)
Picker("video", selection: $selectorIndex) {
Text("Video On").tag(0)
Text("Video Off").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding([.leading, .trailing], 16)
Spacer().frame(minHeight: 8, maxHeight: 50)
Button(action: { self.sendPressed() }) {
ZStack {
RoundedRectangle(cornerRadius: 100)
Text("go")
.font(.custom("Roboto-Bold", size: 36))
.foregroundColor(Color("MeetingGreen"))
}
}
.foregroundColor(Color("MeetingLightGreen").opacity(0.45))
.frame(width: 137, height: 73)
}
Spacer()
}
}
}
Would appreciate any suggestions!
Update: The issue seem to have occured due to overlapping of the View's because the cornerRadius value of RoundedRectangle was set to 100. Bringing the value of cornerRadius to 55 would restore the Picker's functionality.
The issue seems to be caused because of the RoundedRectangle(cornerRadius: 100) within the ZStack. I don't have and explanation as to why this is happening. I'll add the reason if I ever find out. Could be a SwiftUI bug. I can't tell untill I find any related evidence. So here is the code that could make the SegmentedControl work without any issue.
struct MeetingLogin: View {
//...
#State private var selectorIndex: Int = 0
var body: some View {
VStack {
//...
Group {
Spacer().frame(minHeight: 8, maxHeight: 30)
Picker("video", selection: $selectorIndex) {
Text("Video On").tag(0)
Text("Video Off").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.padding([.leading, .trailing], 16)
Spacer().frame(minHeight: 8, maxHeight: 50)
Button(action: { self.sendPressed() }) {
ZStack {
RoundedRectangle(cornerRadius: 55)
Text("go")
.font(.custom("Roboto-Bold", size: 36))
.foregroundColor(Color("MeetingGreen"))
}
}
.foregroundColor(Color("MeetingLightGreen").opacity(0.45))
.frame(width: 137, height: 73)
}
Spacer()
}
}
}
Xcode 12 iOS 14
you can get it by adding a if-else condition on the tapGesture of your Segment Control(Picker)
#State private var profileSegmentIndex = 0
Picker(selection: self.$profileSegmentIndex, label: Text("Jamaica")) {
Text("My Posts").tag(0)
Text("Favorites").tag(1)
}
.onTapGesture {
if self.profileSegmentIndex == 0 {
self.profileSegmentIndex = 1
} else {
self.profileSegmentIndex = 0
}
}
.pickerStyle(SegmentedPickerStyle())
.padding()
If you need to use it with more than 2 segments, you can try with an Enum :)
Your code/View is missing the if-condition to know what shall happen when the selectorIndex is 0 or 1.
It has to look like this:
var body: some View {
VStack {
if selectorIndex == 0 {
//....Your VideoOn-View
} else {
//Your VideoOff-View
}
}

What might be causing this animation bug with SwiftUI and NavigationView?

I've been experimenting with some SwiftUI layouts and one of the things that I wanted to try out was creating a simple circular progress ring. After playing around with the code for a while I managed to get everything working the way I was hoping for it to, at least for a prototype. The issue arrises when I embed this view inside a SwiftUI NavigationView. Now, every time I run the app in the canvas, simulator, or on a device, the initial loading of the progress ring has the entire view slowly sliding up into position.
This is a simple prototype, just messing around with the new SwiftUI tools. After some experimentation, I've found that if I remove the NavigationView the ring acts like it's meant to from the beginning. I'm not seeing an obvious reason for why this issue is occurring though.
import SwiftUI
struct ProgressRing_ContentView: View {
#State var progressToggle = false
#State var progressRingEndingValue: CGFloat = 0.75
var ringColor: Color = Color.green
var ringWidth: CGFloat = 20
var ringSize: CGFloat = 200
var body: some View {
TabView{
NavigationView{
VStack{
Spacer()
ZStack{
Circle()
.trim(from: 0, to: progressToggle ? progressRingEndingValue : 0)
.stroke(ringColor, style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.background(Circle().stroke(ringColor, lineWidth: ringWidth).opacity(0.2))
.frame(width: ringSize, height: ringSize)
.rotationEffect(.degrees(-90.0))
.animation(.easeInOut(duration: 1))
.onAppear() {
self.progressToggle.toggle()
}
Text("\(Int(progressRingEndingValue * 100)) %")
.font(.largeTitle)
.fontWeight(.bold)
}
Spacer()
Button(action: {
self.progressRingEndingValue = CGFloat.random(in: 0...1)
}) { Text("Randomize")
.font(.largeTitle)
.foregroundColor(ringColor)
}
Spacer()
}
.navigationBarTitle("ProgressRing", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
print("Refresh Button Tapped")
}) {
Image(systemName: "arrow.clockwise")
.foregroundColor(Color.green)
}, trailing:
Button(action: {
print("Share Button Tapped")
}) {
Image(systemName: "square.and.arrow.up")
.foregroundColor(Color.green)
}
)
}
}
}
}
#if DEBUG
struct ProgressRing_ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ProgressRing_ContentView()
.environment(\.colorScheme, .light)
.previewDisplayName("Light Mode")
}
}
#endif
Above is the exact code that I'm currently working with. The actual animation of the ring sliding seems to be working how I expected it to, I'm just not sure why the entire ring itself is moving when embedded in a NavigationView.
You need to use explicit animations, instead of implicit. With implicit animations, any animatable parameter that changes, the framework will animate. Whenever possible, you should use explicit animations. Below is the updated code. Notice I remove the .animation() call and added two withAnimation() closures.
If you would like to expand your knowledge on implicit vs. explicit animations, check this link: https://swiftui-lab.com/swiftui-animations-part1/
struct ContentView: View {
#State var progressToggle = false
#State var progressRingEndingValue: CGFloat = 0.75
var ringColor: Color = Color.green
var ringWidth: CGFloat = 20
var ringSize: CGFloat = 200
var body: some View {
TabView{
NavigationView{
VStack{
Spacer()
ZStack{
Circle()
.trim(from: 0, to: progressToggle ? progressRingEndingValue : 0)
.stroke(ringColor, style: StrokeStyle(lineWidth: ringWidth, lineCap: .round, lineJoin: .round))
.background(Circle().stroke(ringColor, lineWidth: ringWidth).opacity(0.2))
.frame(width: ringSize, height: ringSize)
.rotationEffect(.degrees(-90.0))
.onAppear() {
withAnimation(.easeInOut(duration: 1)) {
self.progressToggle.toggle()
}
}
Text("\(Int(progressRingEndingValue * 100)) %")
.font(.largeTitle)
.fontWeight(.bold)
}
Spacer()
Button(action: {
withAnimation(.easeInOut(duration: 1)) {
self.progressRingEndingValue = CGFloat.random(in: 0...1)
}
}) { Text("Randomize")
.font(.largeTitle)
.foregroundColor(ringColor)
}
Spacer()
}
.navigationBarTitle("ProgressRing", displayMode: .inline)
.navigationBarItems(leading:
Button(action: {
print("Refresh Button Tapped")
}) {
Image(systemName: "arrow.clockwise")
.foregroundColor(Color.green)
}, trailing:
Button(action: {
print("Share Button Tapped")
}) {
Image(systemName: "square.and.arrow.up")
.foregroundColor(Color.green)
}
)
}
}
}
}