I'm trying to follow this calculator tutorial but am running into several issues. One of the issues is the use of the .overlay method. I'm assuming it doesn't work because it is deprecated, but I can't figure out how to get the recommeded way or get anything else to work to solve it, so am reaching out for options.
Xcode 12.4
Target: iOS 14.4
Here is the code in that section:
struct CalculatorButtonStyle: ButtonStyle {
var size: CGFloat
var backgroundColor: Color
var foregroundColor: Color
var isWide: Bool = false
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 32, weight: .medium))
.frame(width: size, height: size)
.frame(maxWidth: isWide ? .infinity : size, alignment: .leading)
.background(backgroundColor)
.foregroundColor(foregroundColor)
/* //Commented out to compile
.overlay( {
if configuration.isPressed {
Color(white: 1.0, opacity: 0.2)
}
)
}
*/
//.clipShape(Capsule()) //this makes circle buttons
} //func
} //struct
I've tried commenting out that section which is the only way to have it compile, but then the button press action of showing a different color does not work.
Overlay is not deprecated. Where did you read that. I think your problem is that you try to use the overlay function in a button style, which is not possible. You can only use it in a view. The error you wrote behind it, also states that.
I'm also not sure what you want to achieve, because doesn't it work correct if you just not use the overlay?
I get this button without the overlay. Is that what you need?
With the button style:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Button("1") {
print("being pressed")
}
.buttonStyle(
CalculatorButtonStyle(
size: 40,
backgroundColor: .cyan,
foregroundColor: .black
)
)
}
.padding()
}
}
I also added the changed button style.
import SwiftUI
struct CalculatorButtonStyle: ButtonStyle {
var size: CGFloat
var backgroundColor: Color
var foregroundColor: Color
var isWide: Bool = false
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 32, weight: .medium))
.frame(width: size, height: size)
.frame(maxWidth: isWide ? .infinity : size, alignment: .leading)
.background(backgroundColor)
.foregroundColor(configuration.isPressed ? Color(white: 1.0, opacity: 0.2) : foregroundColor)
.clipShape(Capsule()) //this makes circle buttons
}
}
An overlay is not needed. Take advantage of the isPressed value of the configuration to change the color
struct CalculatorButtonStyle: ButtonStyle {
var size: CGFloat
var backgroundColor: Color
var foregroundColor: Color
var isWide: Bool = false
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 32, weight: .medium))
.frame(width: size, height: size)
.frame(maxWidth: isWide ? .infinity : size, alignment: .leading)
.background(backgroundColor)
.foregroundColor(configuration.isPressed
? Color(white: 1.0, opacity: 0.5)
: foregroundColor)
.clipShape(Capsule())
} //func
} //struct
You have two stray parentheses:
// Remove this
// ↓
.overlay( {
if configuration.isPressed {
Color(white: 1.0, opacity: 0.2)
}
) // ← Remove this
}
The stray left parenthesis (the one immediately after .overlay) prevents Swift from recognizing the trailing closure syntax and makes Swift think you are trying to use the deprecated version of overlay.
The stray right parenthesis is improperly nested relative to the right-brace on the following line.
If you copy and paste the code from the tutorial, or if you remove those two parentheses, the code compiles:
struct CalculatorButtonStyle: ButtonStyle {
var size: CGFloat
var backgroundColor: Color
var foregroundColor: Color
var isWide: Bool = false
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 32, weight: .medium))
.frame(width: size, height: size)
.frame(maxWidth: isWide ? .infinity : size, alignment: .leading)
.background(backgroundColor)
.foregroundColor(foregroundColor)
.overlay {
if configuration.isPressed {
Color(white: 1.0, opacity: 0.2)
}
}
.clipShape(Capsule())
}
}
Related
Problem:I have a View that I needed to place multiple (2) views that contained: 1 Image + 1 Text. I decided to break that up into a ClickableImageAndText structure that I called on twice. This works perfectly if the image is a set size (64x64) but I would like this to work on all size classes. Now, I know that I can do the following:
if horizontalSizeClass == .compact {
Text("Compact")
} else {
Text("Regular")
}
but I am asking for both Different Size Classes and Same Size Classes such as the iPhone X and iPhone 13 which are the same.
Question:How do I alter the image for dynamic phone sizes (iPhone X, 13, 13 pro, etc) so it looks appropriate for all measurements?
Code:
import SwiftUI
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
VStack {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 64, height: 64)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
struct InitialView: View {
var topView: some View {
Image("Empty_App_Icon")
.resizable()
.scaledToFit()
}
var bottomView: some View {
VStack {
ClickableImageAndText(
image: "Card_Icon",
text: "View Your Memories") {
print("Tapped on View Memories")
}
.padding(.bottom)
ClickableImageAndText(
image: "Camera",
text: "Add Memories") {
print("Tapped on Add Memories")
}
.padding(.top)
}
}
var body: some View {
ZStack {
GradientView()
VStack {
Spacer()
topView
Spacer()
bottomView
Spacer()
}
}
}
}
struct InitialView_Previews: PreviewProvider {
static var previews: some View {
InitialView()
}
}
Image Note:My background includes a GradientView that I have since removed (thanks #lorem ipsum). If you so desire, here is the GradientView code but it is unnecessary for the problem above.
GradientView.swift
import SwiftUI
struct GradientView: View {
let firstColor = Color(uiColor: UIColor(red: 127/255, green: 71/255, blue: 221/255, alpha: 1))
let secondColor = Color(uiColor: UIColor(red: 251/255, green: 174/255, blue: 23/255, alpha: 1))
let startPoint = UnitPoint(x: 0, y: 0)
let endPoint = UnitPoint(x: 0.5, y: 1)
var body: some View {
LinearGradient(gradient:
Gradient(
colors: [firstColor, secondColor]),
startPoint: startPoint,
endPoint: endPoint)
.ignoresSafeArea()
}
}
struct GradientView_Previews: PreviewProvider {
static var previews: some View {
GradientView()
}
}
Effort 1:Added a GeometryReader to my ClickableImageAndText structure and the view is automatically changed incorrectly.
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
GeometryReader { reader in
VStack {
Image(image)
.resizable()
.scaledToFit()
.frame(width: 64, height: 64)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
}
Effort 2:Added a GeometryReader as directed by #loremipsum's [deleted] answer and the content is still being pushed; specifically, the topView is being push to the top and the bottomView is taking the entire space with the addition of the GeometryReader.
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
var body: some View {
GeometryReader{ geo in
VStack {
Image(image)
.resizable()
.scaledToFit()
//You can do this and set strict size constraints
//.frame(minWidth: 64, maxWidth: 128, minHeight: 64, maxHeight: 128, alignment: .center)
//Or this to set it to be percentage of the size of the screen
.frame(width: geo.size.width * 0.2, alignment: .center)
Text(text)
}.foregroundColor(.white)
//Everything moves to the left because the `View` expecting a size vs stretching.
//If yo want the entire width just set the View with on the outer most View
.frame(width: geo.size.width, alignment: .center)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
The possible solution is to use screen bounds (which will be different for different phones) as reference value to calculate per-cent-based dynamic size for image. And to track device orientation changes we wrap our calculations into GeometryReader.
Note: I don't have your images, so added white borders for demo purpose
struct ClickableImageAndText: View {
let image: String
let text: String
let tapAction: (() -> Void)
#State private var size = CGFloat(32) // some minimal initial value (not 0)
var body: some View {
VStack {
Image(image)
.resizable()
.scaledToFit()
// .border(Color.white) // << for demo !!
.background(GeometryReader { _ in
// GeometryReader is needed to track orientation changes
let sizeX = UIScreen.main.bounds.width
let sizeY = UIScreen.main.bounds.height
// Screen bounds is needed for reference dimentions, and use
// it to calculate needed size as per-cent to be dynamic
let width = min(sizeX, sizeY)
Color.clear // % (whichever you want)
.preference(key: ViewWidthKey.self, value: width * 0.2)
})
.onPreferenceChange(ViewWidthKey.self) {
self.size = max($0, size)
}
.frame(width: size, height: size)
Text(text)
.foregroundColor(.white)
}
.contentShape(Rectangle())
.onTapGesture {
tapAction()
}
}
}
How could you achieve Icons like that?
I know that the base is this:
Image(systemName: "person.fill")
And than you could give it a background-Color:
Image(systemName: "person.fill")
.background(Color.blue)
To get rounded corners you could just add cornerRadius:
Image(systemName: "person.fill")
.background(Color.blue)
.cornerRadius(5)
But how would you make it that each of the items is in a square box with the same size?
Because SF Symbols don't have the same size.
And I don't want to make this:
Image(systemName: "person.fill")
.frame(width: 20, height: 20)
.background(Color.blue)
.cornerRadius(5)
The frame modifier would destroy the ability of SF Symbols to match with the preferred Font Size of the User.
Is there an other solution?
Or do you think the Settings App is done with .frame()?
Okay, I found an answer at Medium.
He works with Labels and adds an custom Modifier to them.
The Modifier looks like that:
struct ColorfulIconLabelStyle: LabelStyle {
var color: Color
var size: CGFloat
func makeBody(configuration: Configuration) -> some View {
Label {
configuration.title
} icon: {
configuration.icon
.imageScale(.small)
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 7 * size).frame(width: 28 * size, height: 28 * size).foregroundColor(color))
}
}
}
I did some changes:
struct ColorfulIconLabelStyle: LabelStyle {
var color: Color
func makeBody(configuration: Configuration) -> some View {
Label {
configuration.title
} icon: {
configuration.icon
.font(.system(size: 17))
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 7).frame(width: 28, height: 28).foregroundColor(color))
}
}
}
You can use it like that:
NavigationLink {
//Destination
} label: {
Label("Your Text", systemImage: "Your Image").labelStyle(ColorfulIconLabelStyle(color: .green))
}
This achieves a very native look :)
As I mentioned, full credits to Luca J.
I recommend this down way for you the update for my answer would be reading the device is zoomed or not! Then we could gave correct size for your UI, you can change your wished size in class.
struct ContentView: View {
var body: some View {
CustomButtonView(string: "gear", action: { print("setting!") })
CustomButtonView(string: "lasso.sparkles", action: { print("lasso!") })
CustomButtonView(string: "xmark.bin", action: { print("xmark!") })
CustomButtonView(string: "command", action: { print("command!") })
CustomButtonView(string: "infinity", action: { print("infinity!") })
}
}
struct CustomButtonView: View {
let string: String
let action: (() -> Void)?
init(string: String, action: (() -> Void)? = nil) {
self.string = string
self.action = action
}
#State private var tapped: Bool = Bool()
var body: some View {
Image(systemName: string)
.resizable()
.scaledToFit()
.frame(width: DeviceReader.shared.size - 5.0, height: DeviceReader.shared.size - 5.0)
.foregroundColor(Color.white)
.padding(5.0)
.background(tapped ? Color.blue : Color.gray)
.cornerRadius(10.0)
.onTapGesture { tapped.toggle(); action?() }
.animation(.interactiveSpring(), value: tapped)
}
}
class DeviceReader: ObservableObject {
let size: CGFloat
init() {
switch UIDevice.current.userInterfaceIdiom {
case .phone: self.size = 30.0
case .pad: self.size = 40.0
case .mac: self.size = 50.0
default: self.size = 30.0 }
}
static let shared: DeviceReader = DeviceReader()
}
I was looking at this question and your answer to it because this would be a useful thing to have. However, your answer does not allow the SFFont to scale with user preferences, and the answer you found on the Medium post does not scale well, as you can't just scale up and down with theses things. They look weird. If you run it in the simulator and change the Text setting, your will see what I mean.
I would simply use a .frame that changes it's size based off a preference key on the SF Symbol itself, and giving it a bit of padding extra. You could also simply add .padding() before your .background(), but the background would not necessarily be square. This method will set the width and height of the frame to slightly more than the biggest dimension of the SF Symbol, and it will fluidly change its size, not only allowing you to drop a .font() on it, but also handle the dynamic font sizes. This is a pure SwiftUI answer, using no UIKit.
struct ColoredIconView: View {
let imageName: String
let foregroundColor: Color
let backgroundColor: Color
#State private var frameSize: CGSize = CGSize(width: 30, height: 30)
#State private var cornerRadius: CGFloat = 5
var body: some View {
Image(systemName: imageName)
.overlay(
GeometryReader { proxy in
Color.clear
.preference(key: SFSymbolKey.self, value: max(proxy.size.width, proxy.size.height))
}
)
.onPreferenceChange(SFSymbolKey.self) {
let size = $0 * 1.05
frameSize = CGSize(width:size, height: size)
cornerRadius = $0 / 6.4
}
.frame(width: frameSize.width, height: frameSize.height)
.foregroundColor(foregroundColor)
.background(
RoundedRectangle(cornerRadius: cornerRadius)
.fill(backgroundColor)
)
}
}
fileprivate struct SFSymbolKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
Use it like this:
ColoredIconView(imageName: "airplane", foregroundColor: .white, backgroundColor: .orange)
.font(.body)
Here I got 2 shapes, one Rectangle and one Circle, which with action of a Button only one of them became visible to user, I tried to use #Namespace for this transform, but did not panned out!
MY Goal: Having a nice and smooth transform animation from one Shape to other.
struct ContentView: View {
#State var action: Bool = false
#Namespace var sameShape
var body: some View {
ZStack
{
Group
{
if action
{
Circle()
.fill(Color.blue).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "Dakota148Zero", in: sameShape)
}
else
{
Rectangle()
.fill(Color.red).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "Dakota148Zero", in: sameShape)
}
}
.animation(.easeInOut)
VStack
{
Spacer()
Button("transform") { action.toggle() }.font(Font.largeTitle).padding()
}
}
}
}
Here is way to do it by representing the circle and square as a RoundedRectangle with different cornerRadius values:
struct ContentView: View {
#State var action = false
var body: some View {
ZStack
{
RoundedRectangle(cornerRadius: action ? 0 : 75)
.fill(action ? Color.red : .blue)
.frame(width: 150, height: 150, alignment: .center)
.animation(.easeInOut)
VStack
{
Spacer()
Button("transform") {
action.toggle()
}
.font(Font.largeTitle)
.padding()
}
}
}
}
Group is not real container, so don't store animation. Replace Group with some stack, like
VStack
{
if action
// ... other code no change
With iOS14 out, you can use matchedGeometryEffect(). If you are using iOS14, I would recommend this approach.
https://www.hackingwithswift.com/quick-start/swiftui/how-to-synchronize-animations-from-one-view-to-another-with-matchedgeometryeffect
https://developer.apple.com/documentation/swiftui/view/matchedgeometryeffect(id:in:properties:anchor:issource:)
So in your solution, if you replace action.toggle() with withAnimation{self.action.toggle()} in your button code, it will animate.
Button("transform") {
withAnimation{self.action.toggle()}
}
.font(Font.largeTitle).padding()
This solution works on the simulator for me (Xcode 12.1, iPhone 11 iOS 14.1):
import SwiftUI
struct ContentView: View {
#State var action: Bool = false
#Namespace var transition
var body: some View {
ZStack {
Group {
if action {
Circle()
.fill(Color.blue).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "shape", in: transition)
} else {
Rectangle()
.fill(Color.red).frame(width: 150, height: 150, alignment: .center)
.matchedGeometryEffect(id: "shape", in: transition)
}
}
.animation(.easeInOut)
VStack {
Spacer()
Button("transform") { withAnimation{self.action.toggle()} }.font(Font.largeTitle).padding()
}
}
}
}
The matchedGeometryEffect() doesn't want to animate different shapes (including cornerRadius) or colors that nicely, not sure if this is a bug that will get fixed in future patches or just a feature that needs to be worked around by regular animations. With me playing around with matchedGeometryEffect(), it seems to do great with sizing things up and down, like shown with this code:
import SwiftUI
struct ContentView: View {
#State private var animate: Bool = false
#Namespace private var transition
var body: some View {
VStack {
if animate {
RoundedRectangle(cornerRadius: 75.0)
.matchedGeometryEffect(id: "shape", in: transition)
.frame(width: 250, height: 250, alignment: .center)
.foregroundColor(Color.blue)
.animation(.easeInOut)
.onTapGesture {
animate.toggle()
}
} else {
// Circle
RoundedRectangle(cornerRadius: 75.0)
.matchedGeometryEffect(id: "shape", in: transition)
.frame(width: 150, height: 150, alignment: .center)
.foregroundColor(Color.red)
.animation(.easeInOut)
.onTapGesture {
animate.toggle()
}
}
}
}
}
I am trying to constrain image size between min and max. But the view expands both width and height to their max values, removing the original aspect ratio.
import SwiftUI
struct ImageConstrainSizeTest: View {
var body: some View {
Image("bike")
.resizable()
.aspectRatio(contentMode: .fit)
.border(Color.yellow, width: 5)
.frame(minWidth: 10, maxWidth: 300, minHeight: 10, maxHeight: 300, alignment: .center)
.border(Color.red, width: 5)
}
}
struct ImageConstrainSizeTest_Previews: PreviewProvider {
static var previews: some View {
ImageConstrainSizeTest()
}
}
In the screenshot below, I want the red box to shrink to yellow box.
Tried using GeometryReader, but that gives the opposite effect of expanding the yellow box to red box.
Any thoughts?
Here is a demo of possible approach - using view preferences and a bit different layout order (we give area to fit image with aspect and then by resulting image size constrain this area).
Demo prepared & tested with Xcode 11.7 / iOS 13.7
struct ViewSizeKey: PreferenceKey {
typealias Value = CGSize
static var defaultValue: CGSize = .zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue()
}
}
struct ImageConstrainSizeTest: View {
#State private var size = CGSize.zero
var body: some View {
Color.clear
.frame(width: 300, height: 300)
.overlay(
Image("img")
.resizable()
.aspectRatio(contentMode: .fit)
.border(Color.yellow, width: 5)
.background(GeometryReader {
Color.clear.preference(key: ViewSizeKey.self,
value: $0.size) })
)
.onPreferenceChange(ViewSizeKey.self) {
self.size = $0
}
.frame(maxWidth: size.width, maxHeight: size.height)
.border(Color.red, width: 5)
}
}
How do I check to see if dark mode on the device is enabled. I want to check this from within a view and conditionally show or hide a shadow.
I thought I could jus get the colorScheme from the environment but I think I'm missing something.
struct FloatingAddButton : View {
#Environment(\.colorScheme) var colorScheme
#Binding var openAddModal: Bool
var body : some View {
VStack {
Spacer()
HStack() {
Spacer()
Button(action: {
self.openAddModal = true
}) {
ZStack {
Circle()
.foregroundColor(Color(RetroTheme.shared.appMainTint))
.frame(width: 50, height: 50, alignment: .center)
if(self.colorScheme == .light) {
.shadow(color: .secondary, radius: 5, x: 0, y: 0)
}
Image(systemName: "plus")
.foregroundColor(Color.white)
}
} // End Button
}
}
}
}
In my code, I have a simple View extension, that makes the code a lot more readable. With it, I can apply modifiers conditionally:
.conditionalModifier(self.colorScheme == .light, LightShadow())
The full implementation is below:
extension View {
// If condition is met, apply modifier, otherwise, leave the view untouched
public func conditionalModifier<T>(_ condition: Bool, _ modifier: T) -> some View where T: ViewModifier {
Group {
if condition {
self.modifier(modifier)
} else {
self
}
}
}
}
struct FloatingAddButton : View {
#Environment(\.colorScheme) var colorScheme
#Binding var openAddModal: Bool
var body : some View {
VStack {
Spacer()
HStack() {
Spacer()
Button(action: { self.openAddModal = true }) {
ZStack {
Circle()
.foregroundColor(Color(.red))
.frame(width: 50, height: 50, alignment: .center)
.conditionalModifier(self.colorScheme == .light, LightShadow())
Image(systemName: "plus")
.foregroundColor(Color.white)
}
}
} // End Button
}
}
}
struct LightShadow: ViewModifier {
func body(content: Content) -> some View {
content.shadow(color: .secondary, radius: 5, x: 0, y: 0)
}
}
If you ever have a case where you want to apply different modifiers for true and false, here's another extension:
extension View {
// Apply trueModifier if condition is met, or falseModifier if not.
public func conditionalModifier<M1, M2>(_ condition: Bool, _ trueModifier: M1, _ falseModifier: M2) -> some View where M1: ViewModifier, M2: ViewModifier {
Group {
if condition {
self.modifier(trueModifier)
} else {
self.modifier(falseModifier)
}
}
}
}
You are using colorScheme correctly. But it looks like you have a different issue - placing a modifier inside an if statement. I found that, unlike a View, modifiers don't work that way.
The answer is to create a custom ViewModifier. In your case I'd package everything up into one modifier like this:
struct CircleStyle: ViewModifier {
#Environment (\.colorScheme) var colorScheme:ColorScheme
func body(content: Content) -> some View {
if colorScheme == .light {
return content
.foregroundColor(Color(RetroTheme.shared.appMainTint))
.frame(width: 50, height: 50, alignment: .center)
.shadow(color: .secondary, radius: 5, x: 0, y: 0)
} else {
return content
.foregroundColor(Color(RetroTheme.shared.appMainTint))
.frame(width: 50, height: 50, alignment: .center)
}
}
And to use it:
Circle()..modifier(CircleStyle())
If you need to add more variables from your model, simply pass it into your modifier.
Thanks to #dfd for pointing out that I can't use an if statement with a modifier. I updated my code like this for now. This just returns different versions of the circle in light and dark mode.
if colorScheme == .light {
Circle()
.foregroundColor(Color(RetroTheme.shared.appMainTint))
.frame(width: 50, height: 50, alignment: .center)
.shadow(color: .secondary, radius: 5, x: 0, y: 0)
} else {
Circle()
.foregroundColor(Color(RetroTheme.shared.appMainTint))
.frame(width: 50, height: 50, alignment: .center)
}
SwiftUI
With the \.colorScheme key of an Environment variable:
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
var body: some View {
Text(colorScheme == .dark ? "In dark mode" : "In light mode")
}
}
Also, it automatically updates on the change of the environment color scheme.
UIKit
To check the current, all object those conform to UITraitEnvironment protocol, including all UIView subclasses and all UIViewConttroller subclasses have access to the current style:
myUIView.traitCollection.userInterfaceStyle == .dark
myUIViewController.traitCollection.userInterfaceStyle == .dark
To detect the change of the style, here is the full detailed answer
SwiftUI makes it really simply to detect when dark mode is enabled. We simply have to add a #Enviroment variable and use .colorScheme property to scan the settings on our device and see if dark mode is enabled.
Let's take a look at the example below.
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
var body: some View {
ZStack {
Color(colorScheme == .light ? .blue : .red)
Text("Hello, World!")
}
}
}
In the code above we are creating the #Environment variable to see if our device is in dark mode. Then inside of our body view we are setting the background color to red if its in dark mode or blue if its not in dark mode by using our colorScheme variable inside of a ternary operator.
A great use case for this is if you want to support different custom UI's for when the users device is in dark mode.
Happy Coding ;