How to adjust the size of shapes in order to fit the screen and align them to center? - swift

In the app I am working on, I have used PocketSVG to read paths stored in .svg file the following way (init):
class ColoringImageViewModel: ObservableObject {
#Published var shapeItemsByKey = [UUID: ShapeItem]()
var shapeItemKeys: [UUID] = []
init(selectedImage: String) {
let svgURL = Bundle.main.url(forResource: selectedImage, withExtension: "svg")!
let _paths = SVGBezierPath.pathsFromSVG(at: svgURL)
for (index, path) in _paths.enumerated() {
let scaledBezier = ScaledBezier(bezierPath: path)
let shapeItem = ShapeItem(path: scaledBezier)
shapeItemsByKey[shapeItem.id] = shapeItem
shapeItemKeys.append(shapeItem.id)
}
}
}
struct ShapeItem: Identifiable {
let id = UUID()
var color: Color = Color.white
var path: ScaledBezier
init(path: ScaledBezier) {
self.path = path
}
}
After reading the paths, I transform them to shapes and place every shape as a ShapeView on ZStack:
struct ShapeView: View {
#Binding var shapeItem: ShapeItem?
var body: some View {
ZStack {
shapeItem!.path
.fill(shapeItem!.color)
shapeItem?.path.stroke(Color.black)
}
}
}
struct ColoringImageView: View {
...
var body: some View {
GeometryReader {
geometry in
ZStack {
ForEach(coloringImageViewModel.shapeItemKeys, id: \.self){ id in
ShapeView(shapeItem: $coloringImageViewModel.shapeItemsByKey[id])
}
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
.background(Color.white)
.contentShape(Rectangle())
.edgesIgnoringSafeArea(.all)
}
}
...
}
On the screen, the paths are placed as shown below:
Instead, I would like to place an arbitrary vector image (split to paths) in the middle of the screen and scale it such that it fits the screen as shown below:
I would appreciate any suggestion to modify the code or implement the additional logic in order to fit the screen and align the image.
Edit
After replacing
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
with
.frame(maxWidth: .infinity, maxHeight: .infinity)
the image is placed as shown below:
It seems like vertically the image is centered although I'm not sure as horizontally it doesn't seem to be centered. Also, I haven't found a solution to fit the image to screen (the example image doesn't fit the screen).

Instead of GeometryReader use the max frame for ZStack as follows
var body: some View {
ZStack {
Color.clear // this gives full screen ZStack
ForEach(coloringImageViewModel.shapeItemKeys, id: \.self){ id in
ShapeView(shapeItem: $coloringImageViewModel.shapeItemsByKey[id])
}
}
.background(Color.white)
.contentShape(Rectangle())
.edgesIgnoringSafeArea(.all)
}

Related

SwiftUI - Creating a Star Rating View with decimal point fillable foreground colour in

I am trying to develop a star rating view using SwiftUI. I have a maximum of 5 stars, and the tricky part is that I need to fill 4.7 stars. So the last star should only fill up 70% of its foreground color.
Below is the code that I have done so far
import SwiftUI
struct AverageStarsView: View {
#Binding var rating: Double
var body: some View {
Group {
StarView()
}
.frame(maxHeight: 16)
}
}
struct AverageStarsView_Previews: PreviewProvider {
static var previews: some View {
AverageStarsView(rating: .constant(4.7))
}
}
struct StarView: View {
private var fillColor = .yellow
private var emptyColor = .grey
var body: some View {
ZStack {
if let starImage = UIImage(named: "icon-star", in: .sharedResources, compatibleWith: nil) {
Image(uiImage: starImage)
.resizable()
.renderingMode(.template)
.foregroundColor(emptyColor)
.aspectRatio(contentMode: .fit)
}
}
}
}
struct StarView_Previews: PreviewProvider {
static var previews: some View {
StarView()
}
}
How can fill only 70% of the star view.
I can't use .overlay because I need to support iOS 14.
Sample view
You can use the .mask() modifier that will create a mask of your view with a given view. Here is what you can achieve for a single star:
struct StarView: View {
var fillValue: Double
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
Rectangle()
.fill(Color.orange)
.frame(width: geometry.size.width * fillValue)
}
}
.mask(
Image(systemName: "star.fill")
.resizable()
)
.aspectRatio(1, contentMode: .fit)
}
}
Here is the result with the fillValue set to 0.7:
You can then stack them into an HStack and make a quick calculation to know the fill value of the last star to make your five stars rating component.
Make sure to use .mask() and not .mask { } as the last one require iOS 15.0 to be used.

How to get text to be centered within ZStack{ Rectangle} within LazyVGrid

//Im Trying to make a grid that displays rectangles with text inside, but the text is cut off to the right of //the rectangle. How do I get the Text to display properly within the rectangles?
struct TileView: View{
let tile: Model.Tile
var body: some View{
ZStack(alignment: Alignment(horizontal: .leading, vertical: .center)){
let tileVisual = Rectangle()
if tile.isSelected{
tileVisual.fill().foregroundColor(.white)
tileVisual.strokeBorder(lineWidth:3)
let tileText = Text("\(tile.numberOfNearbyBombs)")//declaration here
tileText
.frame(alignment: .leading)
}
else{
tileVisual.fill().foregroundColor(.blue)
tileVisual.strokeBorder(lineWidth:3)
//tileVisual.foregroundColor(.blue)
}
}
.frame(width: ViewModel.widthProportion, height: ViewModel.heightProportion)
}
}
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
let formatting = ViewModel.getGridItems(amount: ViewModel.boardHeight)
ScrollView{
ZStack{
LazyVGrid(columns: formatting, spacing: 0){
ForEach(viewModel.tiles){
tile in TileView(tile: tile)
.onTapGesture {
viewModel.pressTile(tile)
}
}
}
}
}
}
}
Im Trying to make a grid that displays rectangles with text inside, but the text is cut off to the right of the rectangle. How do I get the Text to display properly within the rectangles? I've tried changing the font size but it always is off-center.

How to know if element comes out the screen in swiftUI?

I try to know if an element comes out the screen in my application. I see that when my element it outside the screen, onDesappear is not trigger. I don't know if there is an other solution to trigger this?
I made an example to explain what I want. I have a circle with an offset to force it out of the screen to a certain degree:
struct ContentView: View {
#ObservedObject var location: LocationProvider = LocationProvider()
#State var heading: Double = 0
var body: some View {
ZStack {
Circle()
.frame(width: 30, height: 30)
.background(Color.red)
.clipShape(Circle())
.foregroundColor(Color.clear)
.offset(y: 300)
.border(Color.black)
.rotationEffect(.degrees(self.heading))
.onReceive(self.location.heading) { heading in
self.heading = heading
}
.onDisappear(perform: { print("Desappear") })
Text("\(self.heading)")
}
}
}
Maybe it's possible with geometryReader?

ButtonAction overwrites preceding ButtonActions

I have an Imageview that has six buttons on top of it. Depending on which button is being clicked, I want to change the image. However, somehow the ButtonActions don't work as expected and I wasn't able to figure out how to change the behavior correctly.
The ZStack for the Overlay looks like that:
class ImageName: ObservableObject {
#Published var name: String = "muscleGuy"
}
struct MuscleGuy: View {
#ObservedObject var imageName = ImageName()
var body: some View {
VStack(alignment: .center) {
Spacer()
ZStack(alignment: .center) {
GeometryReader { geometry in
//the buttons, overlaying the image
ButtonActions( imageName: self.imageName).zIndex(10)
//the image
ImageView(imageName: self.imageName, size: geometry.size).zIndex(2)
.position(x: geometry.size.width/2, y: geometry.size.height/2)
}
}
}
}
}
And to aggregate the different buttons, I created the ButtonActions-struct:
struct ButtonActions: View {
#State var imageName: ImageName
var body: some View {
VStack{
GeometryReader { geometry in
HStack {
Button(action: {
self.imageName.name = "arms_chest"
print(self.imageName.name)
}) {
armsRectangle(size: geometry.size)
}
Button(action: {
self.imageName.name = "abs_lower_back"
print(self.imageName.name)
}) {
absRectangle(size: geometry.size)
}
}
Button(action: {
self.imageName.name = "legs"
print(self.imageName.name)
}) {
legRectangle(size: geometry.size)
}
}
}
}
}
The legRectangle contains one rectangle only, whereas the armsRectangle contains three Rectangles and the absRectangle contains two. They are always nested in a HStack.
I don't want to overload my question with code, so here is one example Rectangle:
struct absRectangle: View {
var size: CGSize
var body: some View {
HStack {
Rectangle().foregroundColor(.red)
.frame(width: size.width/8 + 8, height: size.height/9)
.position(x: -size.width/4, y: size.height/2 - 40)
Rectangle().foregroundColor(.red)
.frame(width: size.width/8 + 8, height: size.height/9)
.position(x: -10, y: size.height/2 - 40)
}
}
}
The behavior that occurs is that the legRectangle always takes over the whole screen. So wherever I tab, the legRectangle-action is being executed.
When commenting that section out and assigning the same zIndex to both the armsRectangle and the absRectangle, it recognizes different tap actions. But the behavior isn't correct either. For example only the right ab-rectangle-action is triggered and the left one again also leads to the arms-rectangle-action.
I feel like the behavior has something to do with the alignment within the HStacks/VStacks/ZStack but I can't figure out what exactly is wrong.

SwiftUI set position to center of different view

I have two different views, one red rect and one black rect that is always positioned on the bottom of the screen. When I click the red rect it should position itself inside the other rect.
Currently the red rect is positioned statically: .position(x: self.tap ? 210 : 50, y: self.tap ? 777 : 50). Is there a way to replace the 210 and 777 dynamically to the position of the black rects center position?
I know that I can use the GeometryReader to get the views size, but how do I use that size to position a different view? Would this even be the right way?
struct ContentView: View {
#State private var tap = false
var body: some View {
ZStack {
VStack {
Spacer()
RoundedRectangle(cornerRadius: 10)
.frame(maxWidth: .infinity, maxHeight: 50, alignment: .center)
}
.padding()
VStack {
ZStack {
Rectangle()
.foregroundColor(.red)
Text("Click me")
.fontWeight(.light)
.foregroundColor(.white)
}
.frame(width: 50, height: 50)
.position(x: self.tap ? 210 : 50, y: self.tap ? 777 : 50)
.onTapGesture {
withAnimation {
self.tap.toggle()
}
}
}
}
}
}
First define some structure where to store .center position of some View
struct PositionData: Identifiable {
let id: Int
let center: Anchor<CGPoint>
}
The build-in mechanism to save such data and expose them to parent View is to set / read (or react) on values which conforms to PreferenceKey protocol.
struct Positions: PreferenceKey {
static var defaultValue: [PositionData] = []
static func reduce(value: inout [PositionData], nextValue: () -> [PositionData]) {
value.append(contentsOf: nextValue())
}
}
To be able to read the center positions of View we can use well known and widely discussed GeometryReader. I define my PositionReader as a View and here we can simply save its center position in our preferences for further usage. There is no need to translate the center to different coordinate system. To identify the View its tag value must be saved as well
struct PositionReader: View {
let tag: Int
var body: some View {
// we don't need geometry reader at all
//GeometryReader { proxy in
Color.clear.anchorPreference(key: Positions.self, value: .center) { (anchor) in
[PositionData(id: self.tag, center: anchor)]
}
//}
}
}
To demonstrate how to use all this together see next simple application (copy - paste - run)
import SwiftUI
struct PositionData: Identifiable {
let id: Int
let center: Anchor<CGPoint>
}
struct Positions: PreferenceKey {
static var defaultValue: [PositionData] = []
static func reduce(value: inout [PositionData], nextValue: () -> [PositionData]) {
value.append(contentsOf: nextValue())
}
}
struct PositionReader: View {
let tag: Int
var body: some View {
Color.clear.anchorPreference(key: Positions.self, value: .center) { (anchor) in
[PositionData(id: self.tag, center: anchor)]
}
}
}
struct ContentView: View {
#State var tag = 0
var body: some View {
ZStack {
VStack {
Color.green.background(PositionReader(tag: 0))
.onTapGesture {
self.tag = 0
}
HStack {
Rectangle()
.foregroundColor(Color.red)
.aspectRatio(1, contentMode: .fit)
.background(PositionReader(tag: 1))
.onTapGesture {
self.tag = 1
}
Rectangle()
.foregroundColor(Color.red)
.aspectRatio(1, contentMode: .fit)
.background(PositionReader(tag: 2))
.onTapGesture {
self.tag = 2
}
Rectangle()
.foregroundColor(Color.red)
.aspectRatio(1, contentMode: .fit)
.background(PositionReader(tag: 3))
.onTapGesture {
self.tag = 3
}
}
}
}.overlayPreferenceValue(Positions.self) { preferences in
GeometryReader { proxy in
Rectangle().frame(width: 50, height: 50).position( self.getPosition(proxy: proxy, tag: self.tag, preferences: preferences))
}
}
}
func getPosition(proxy: GeometryProxy, tag: Int, preferences: [PositionData])->CGPoint {
let p = preferences.filter({ (p) -> Bool in
p.id == tag
})[0]
return proxy[p.center]
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The code is almost self explanatory, we use .background(PositionReader(tag:)) to save the center position of View (this could be avoided by applying .anchorPreference directly on the View) and
.overlayPreferenceValue(Positions.self) { preferences in
GeometryReader { proxy in
Rectangle().frame(width: 50, height: 50).position( self.getPosition(proxy: proxy, tag: self.tag, preferences: preferences))
}
}
is used to create small black rectangle which will position itself at center of other Views. Just tap anywhere in green or red rectangles, and the black one will move immediately :-)
Here is view of this sample application running.
Here is possible approach (with a bit simplified your initial snapshot and added some convenient View extension).
Tested with Xcode 11.2 / iOS 13.2
extension View {
func rectReader(_ binding: Binding<CGRect>, in space: CoordinateSpace) -> some View {
self.background(GeometryReader { (geometry) -> AnyView in
let rect = geometry.frame(in: space)
DispatchQueue.main.async {
binding.wrappedValue = rect
}
return AnyView(Rectangle().fill(Color.clear))
})
}
}
struct ContentView: View {
#State private var tap = false
#State private var bottomRect: CGRect = .zero
var body: some View {
ZStack(alignment: .bottom) {
RoundedRectangle(cornerRadius: 10)
.frame(maxWidth: .infinity, maxHeight: 50, alignment: .center)
.padding()
.rectReader($bottomRect, in: .named("board"))
Rectangle()
.foregroundColor(.red)
.overlay(Text("Click me")
.fontWeight(.light)
.foregroundColor(.white)
)
.frame(width: 50, height: 50)
.position(x: self.tap ? bottomRect.midX : 50,
y: self.tap ? bottomRect.midY : 50)
.onTapGesture {
withAnimation {
self.tap.toggle()
}
}
}.coordinateSpace(name: "board")
}
}