Scaled Metric not working on Apple Watch? - swiftui

I'm trying to do this targeting Apple Watch:
#ScaledMetric var widthScale: CGFloat = 1
and then apply the widthScale to the frame of some SwiftUI View elements:
.frame(width: myFrame.width * widthScale, height: myHeight)
Only it doesn't change at all. Font Sizes do change all around but the widthScale always stays # 1.0.
Did I get the idea wrong? Did I find a bug? Is it not supposed to be used on WatchOS?

So what I found out by trial and error:
The #ScaledMetric should be defined inside the View it is applied in, not in a struct Type somewhere else.
#ScaledMetric values are rounded to whole pixels. So depending on the display density x2 or x3 it's rounded to 1/2halfs or 1/3rds.
Use this view and the previews to see the different scales in action:
import SwiftUI
struct ScaledMetricText: View {
#ScaledMetric(relativeTo: .caption) var widthScaleHundred: CGFloat = 100.0
#ScaledMetric(relativeTo: .caption) var widthScaleTen: CGFloat = 10.0
#ScaledMetric(relativeTo: .caption) var widthScaleOne: CGFloat = 1.0
var body: some View {
VStack {
Text("Unscaled")
.fixedSize(horizontal: true, vertical: false)
.frame(width: 100)
.background(Color(.lightGray))
.padding(3)
Text("100 -> \(widthScaleHundred)")
.fixedSize(horizontal: true, vertical: false)
.frame(width: 100 * widthScaleHundred / 100)
.background(Color(.lightGray))
Text("10 -> \(widthScaleTen)")
.fixedSize(horizontal: true, vertical: false)
.frame(width: 100 * widthScaleTen / 10)
.background(Color(.lightGray))
.padding(3)
Text("1 -> \(widthScaleOne)")
.fixedSize(horizontal: true, vertical: false)
.frame(width: 100 * widthScaleOne)
.background(Color(.lightGray))
}
.font(.caption)
}
}
struct ScaledMetricText_Previews: PreviewProvider {
static var previews: some View {
ScaledMetricText()
.previewDevice(PreviewDevice(rawValue: "Apple Watch Series 6 - 40mm"))
.environment(\.sizeCategory, ContentSizeCategory.small)
ScaledMetricText()
.previewDevice(PreviewDevice(rawValue: "Apple Watch Series 3 - 38mm"))
.environment(\.sizeCategory, ContentSizeCategory.extraExtraExtraLarge)
ScaledMetricText()
.previewDevice(PreviewDevice(rawValue: "Apple Watch Series 6 - 44mm"))
.environment(\.sizeCategory, ContentSizeCategory.small)
ScaledMetricText()
.previewDevice(PreviewDevice(rawValue: "Apple Watch Series 3 - 42mm"))
.environment(\.sizeCategory, ContentSizeCategory.extraExtraExtraLarge)
}
}
Or iOS Previews:
struct ScaledMetricText_Previews: PreviewProvider {
static var previews: some View {
ScaledMetricText()
.previewDevice(PreviewDevice(rawValue: "iPhone 8"))
.environment(\.sizeCategory, ContentSizeCategory.small)
ScaledMetricText()
.previewDevice(PreviewDevice(rawValue: "iPhone 8"))
.environment(\.sizeCategory, ContentSizeCategory.extraExtraExtraLarge)
ScaledMetricText()
.previewDevice(PreviewDevice(rawValue: "iPhone 12 Pro Max"))
.environment(\.sizeCategory, ContentSizeCategory.small)
ScaledMetricText()
.previewDevice(PreviewDevice(rawValue: "iPhone 12 Pro Max"))
.environment(\.sizeCategory, ContentSizeCategory.extraExtraExtraLarge)
}
}

With the code below found that it does work. In principle, in Simulator and on my Watch. Still have to figure out why the problem above won't work.
struct TextLabelsView: View {
var body: some View {
ScrollView(.vertical) {
DynamicResizingView()
}
}
}
struct DynamicResizingView: View {
#ScaledMetric(relativeTo: .largeTitle) var scaledLargeTitle: CGFloat = 10
#ScaledMetric(relativeTo: .title) var scaledTitle: CGFloat = 10
#ScaledMetric(relativeTo: .headline) var scaledHeadline: CGFloat = 10
#ScaledMetric(relativeTo: .subheadline) var scaledSubheadline: CGFloat = 10
#ScaledMetric(relativeTo: .body) var scaledBody: CGFloat = 10
#ScaledMetric(relativeTo: .callout) var scaledCallout: CGFloat = 10
#ScaledMetric(relativeTo: .caption) var scaledCaption: CGFloat = 10
#ScaledMetric(relativeTo: .footnote) var scaledFootnote: CGFloat = 10
#ScaledMetric() var scaledNothing: CGFloat = 10
var body: some View {
VStack(alignment: .leading) {
Text("Dynamic Base10").font(Font.system(size:10))
ScaledText(label: "LargeTitle", size: scaledLargeTitle)
ScaledText(label: "Title", size: scaledTitle)
ScaledText(label: "Headline", size: scaledHeadline)
ScaledText(label: "Subheadline", size: scaledSubheadline)
ScaledText(label: "Body", size: scaledBody)
ScaledText(label: "Callout", size: scaledCallout)
ScaledText(label: "Caption", size: scaledCaption)
ScaledText(label: "Footnote", size: scaledFootnote)
ScaledText(label: "Not Specified", size: scaledNothing)
}
}
}
struct ScaledText: View {
var label: String
var size: CGFloat
var body: some View {
Text("Scaled by \(label) to \(size, specifier: "%.2f")")
.font(Font.system(size: size))
}
}
struct TextLabelsView_Previews: PreviewProvider {
static var previews: some View {
TextLabelsView()
.environment(\.sizeCategory, ContentSizeCategory.accessibilityExtraExtraExtraLarge)
TextLabelsView()
.environment(\.sizeCategory, ContentSizeCategory.small)
}
}
With this you can see the different scaling factors for the different Font.TextStyle|s.

Related

Can't resize view with matchedGeometryEffect

I'm having problems with resizing the same view when applying to it .matchedGeometryEffect. As expected first you apply .matchedGeometryEffect and after it a .frame() view modifier and it has no effect whatsoever. I'm using iOS 16 and Xcode 14. How to overcome this?
Here's code:
import SwiftUI
struct ContentView: View {
let colors: [Color] = [.red, .purple, .yellow, .green, .blue, .mint, .orange]
#State private var colums = Array(repeating: GridItem(), count: 2)
#State private var showNewView = false
#State private var viewNumber = 0
#Namespace private var colorNamespace
var body: some View {
ScrollView {
LazyVGrid(columns: colums) {
ForEach(1..<101) { number in
colors[number % colors.count]
.matchedGeometryEffect(id: number, in: colorNamespace)
.frame(height: 200)
.overlay(Text("\(number)").font(.title2.bold()).foregroundColor(.white))
.onTapGesture {
withAnimation(.spring()) {
viewNumber = number
showNewView.toggle()
}
}
}
}
}
.opacity(showNewView ? 0 : 1)
if showNewView {
colors[viewNumber % colors.count]
.matchedGeometryEffect(id: viewNumber, in: colorNamespace, anchor: .center)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.overlay(Text("\(viewNumber)").font(.title2.bold()).foregroundColor(.white))
.onTapGesture(count: 2) {
withAnimation(.spring()) {
showNewView = false
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How would you make Icons like in the Settings App (SwiftUI)

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)

SwiftUI DragGesture blocking List vertical scroll?

i am trying to add swipe inside list cell , swipe to show more options such as Delete, archive etc .
The swipe is working just fine , but the List ( vertical scroll ) is no longer scrolling up down .
Cell Bite :
import SwiftUI
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
//Swipe to custom options ,by "Jack" this option not yet available in SwiftUI
let drag = DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged {
if (self.offset.width > 0 ){ return }
self.offset.width = $0.translation.width
}.onEnded {
if $0.translation.width < -100 {
self.offset = .init(width: -100, height: 0)
} else {
self.offset = .zero
}
}
ZStack{
Rectangle().foregroundColor(.blue).offset(x: offset.width, y: offset.height)
.gesture(drag)
.animation(.easeIn, value: offset)
Text("test").foregroundColor(.white)
}.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 100,
maxHeight: .infinity,
alignment: .topLeading
)
}
}
struct cl_task_Previews: PreviewProvider {
static var previews: some View {
cl_task().previewLayout(.sizeThatFits)
}
}
List main view :
struct Item {
let uuid = UUID()
let value: String
}
struct w_tasks: View {
#State private var items = [Item]()
var body: some View {
ZStack {
List(self.items, id: \.uuid) {item in
cl_task()
}
.simultaneousGesture(DragGesture().onChanged({ value in
//Scrolled
}))
VStack {
Spacer()
HStack {
Spacer()
Button(action: {
self.items.append(Item(value: "Item"))
}, label: {
Text("+")
.font(.system(size: 50))
.frame(width: 77, height: 70)
.foregroundColor(Color.white)
.padding(.bottom, 7)
})
.background(Color(hex : "#216D94"))
.cornerRadius(38.5)
.padding()
.shadow(color: Color.black.opacity(0.3),
radius: 3,
x: 3,
y: 3)
}
}
}
}
}
struct w_tasks_Previews: PreviewProvider {
static var previews: some View {
w_tasks()
}
}
I've posted my question after spending hours solving this issue as i am new to SwiftUI , any advice how to solve it ?
The solution is to give different distance for swipe, like below
struct cl_task: View {
#State private var offset: CGSize = .zero
var body: some View {
// give 25 distance makes vertical scroll enabled !!
let drag = DragGesture(minimumDistance: 25, coordinateSpace: .local)
.onChanged {
Tested with Xcode 12.4 / iOS 14.4

How to drag across Views in a LazyVGrid with GeometryReader?

I want to drag across rectangles in a grid and change their color. This code is almost working, but this is not always the right rectangle that reacts: it behaves rather randomly. Any hints?
import SwiftUI
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color] = Array(repeating: Color.gray, count: 4)
#State private var rect = [CGRect]()
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect.insert(geo.frame(in: .global), at: rect.endIndex)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}
If I correctly understood your goal, here is fixed variant (with small modifications to avoid repeated hardcoding).
Tested with Xcode 12.1 / iOS 14.1
struct ContentView: View {
let data = (0...3)
#State private var colors: [Color]
#State private var rect: [CGRect]
init() {
_colors = State(initialValue: Array(repeating: .gray, count: data.count))
_rect = State(initialValue: Array(repeating: .zero, count: data.count))
}
var columns: [GridItem] =
Array(repeating: .init(.fixed(70), spacing: 1), count: 2)
var body: some View {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(data, id: \.self) { item in
Rectangle()
.fill(colors[item])
.frame(width: 70, height: 70)
.overlay(
GeometryReader{ geo in
Color.clear
.onAppear {
rect[item] = geo.frame(in: .global)
}
}
)
}
}
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged({ (value) in
if let match = rect.firstIndex(where: { $0.contains(value.location) }) {
colors[match] = Color.red
}
})
)
}
}

Why won't my UI reflect a change in the #State property?

I'm trying to make a graph app, but animating it using a #State property does not help, for some reason.
struct GraphBars: View {
#State var percent: CGFloat
var body: some View {
Capsule()
.fill(Color.black)
.frame(width: 50, height: self.percent)
}
}
struct TEST3: View {
#State var bar1: CGFloat = 90.0
var body: some View {
GeometryReader { gg in
VStack {
Button(action: {
self.bar1 = 300.0
}) {
Text("Hello")
}
GraphBars(percent: bar1)
}
However, pressing the button does not increase the height of the bar as I thought it would. What am I doing wrong?
You need to use #Binding to transfer a variable back and forth between two Views otherwise the parent View doesn't get a notice of the variable change.
Do it like this:
struct GraphBars: View {
#Binding var percent: CGFloat
var body: some View {
Capsule()
.fill(Color.black)
.frame(width: 50, height: self.percent)
}
}
struct Test3: View {
#State var bar1: CGFloat = 90.0
var body: some View {
GeometryReader { gg in
VStack {
Button(action: {
self.bar1 = 300.0
}) {
Text("Hello")
}
GraphBars(percent: self.$bar1)
}
}
}
}