SwiftUI, apply drawGesture only when over Image - swift

I'm try to apply the gesture only when the user is over the Image display not when tapped outside the image.
Any suggestion how I can do? this following code draw also when user tap outside the image.
struct ContentView: View {
#StateObject var am = AppManager()
#State var switchToTakePicture = false
#State var paths: [PathContainer] = []
#State var currentDraggingId = UUID()
#State var spikoPic = #imageLiteral(resourceName: "spiko-16.jpeg")
#State var centerOFimage = CGSize(width: 0, height: 0)
var body: some View {
GeometryReader { proxy in
ZStack {
Image(uiImage: spikoPic)
.resizable()
.scaledToFit()
.position(x: proxy.size.width/2, y: proxy.size.height/2)
.background(GeometryReader {
Color.clear.preference(key: ViewRectKey.self,
value: [$0.frame(in: .global)])
})
.gesture(drawGesture) // not correct
ForEach(paths) { container in
// draw and set the foreground color of the paths to red
container.path
.stroke(Color.red, lineWidth: 4)
}
}
.onPreferenceChange(ViewRectKey.self) { rects in
print(rects.first ?? .zero)
}
}
}
var drawGesture: some Gesture {
DragGesture(minimumDistance: 0)
.onChanged { value in
// The point that the gesture started from
let start = value.startLocation
// The point that the gesture ended to
let end = value.location
// the properties of the rectangle to be drawn
let rectangle: CGRect = .init(origin: end,
size: .init(width: start.x - end.x,
height: start.y - end.y))
// create a path for the rectangle
let path: Path = .init { path in
path.addRect(rectangle)
}
print("rettangolo = \(rectangle) orig \(rectangle.origin) - height \(rectangle.height) width = \(rectangle.width)")
// remove the previous rectangle that was drawen in current
// process of drawing
paths.removeAll { $0.id == currentDraggingId }
// append the new rectangle
paths.append(.init(id: currentDraggingId, path: path))
}
.onEnded { _ in
// renew the dragging id so the app know that the next
// drag gesture is drawing a completely new rectangle,
// and is not continuing the drawing of the last rectangle
currentDraggingId = .init()
}
}
}
i want no box outside

Related

Unable to get pixel color from an image in SwiftUI

I want to design a draggable color selector view that will select the color of the present pixel. My strategy is, first, to choose the position of the pixel and then, second, get the pixel color. The position has been correctly changed while dragging the view. The color of the view has also been changed but don't understand its manner of changing. I think it doesn't returns the proper color of its pixel. Additionally, when x or y of the CGImage coordinate is less then 0, the getPixelColor() returns 'UIExtendedSRGBColorSpace 0 0 0 0'.
After a long search, I have not get any proper solution to get pixel color in SwiftUI.
The program is given below:
struct ColorSelectorView: View {
#Binding var position: CGPoint
#State private var isDragging = false
#State private var colorPicked = UIColor.black
var image: UIImage?
var colorPickedCallBack : (UIColor) -> ()
var body: some View {
colorSelectorView()
.onAppear {
colorPicked = getPixelColor(image: image, point: position) ?? .clear
}
.gesture(DragGesture()
.onChanged { value in
self.position.x += CGFloat(Int(value.location.x - value.startLocation.x))
self.position.y += CGFloat(Int(value.location.y - value.startLocation.y))
colorPicked = getPixelColor(image: image, point: position) ?? .clear
}
.onEnded({ value in
colorPickedCallBack(colorPicked)
})
)
.offset(x: position.x, y: position.y)
}
}
func getPixelColor(image: UIImage, point: CGPoint) -> UIColor? {
guard let pixelData = image.cgImage?.dataProvider?.data else { return nil }
let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)
let pixelInfo: Int = Int((image.size.width * point.y + point.x) * 4.0)
let i = Array(0 ... 3).map { CGFloat(data[pixelInfo + $0]) / CGFloat(255) }
return UIColor(red: i[0], green: i[1], blue: i[2], alpha: 1)
}
}

Canvas doesn't get redrawn in SwiftUI

I have a project in SwiftUI on macOS where I draw to a canvas twice per second.
This is my ContentView:
struct ContentView: view {
#State var score: Int = 0
var body: some View {
VStack {
Text("Score: \(self.score)")
.fixedSize(horizontal: true, vertical: true)
Canvas(renderer: { gc, size in
start(
gc: &gc,
size: size
onPoint: { newScore in
self.score = newScore
}
)
)
}
}
}
The start function:
var renderer: Renderer
func start(
gc: inout GraphicsContext,
size: size,
onPoint: #escaping (Int) -> ()
) {
if renderer != nil {
renderer!.set(gc: &gc)
} else {
renderer = Renderer(
context: &gc,
canvasSize: size,
onPoint: onPoint
)
startGameLoop(renderer: renderer!)
}
renderer!.drawFrame()
}
var timer: Timer
func startGameLoop(renderer: Renderer) {
timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: {
renderer!.handleNextFrame()
}
}
And the renderer roughly looks like this:
class Renderer {
var gc: GraphicsContext
var size: CGSize
var cellSize: CGFloat
let pointCallback: (Int) -> ()
var player: (Int, Int) = (0,0)
init(
context: inout GraphicsContext,
canvasSize: CGSize,
onPoint: (Int) -> ()
) {
self.gc = gc
self.size = canvasSize
self.pointCallback = onPoint
self.cellSize = min(self.size.width, self.size.height)
}
}
extension Renderer {
func handleNextFrame() {
self.player = (self.player.0 + 1, self.player.1 + 1)
self.drawFrame
}
func drawFrame() {
self.gc.fill(
Path(
x: CGFloat(self.player.0) * self.cellSize,
y: CGFloat(self.player.1) * self.cellSize,
width: self.cellSize,
height: self.cellSize
)
)
}
}
So the handleNextFrame method is called twice per second, which calls the drawFrame method, drawing the position of the player to the canvas.
However, there is nothing being drawn to the canvas.
Only the first frame is drawn, which comes from the renderer!.drawFrame() in start. When a point is scored, the canvas is also redrawn, because the start function gets called again.
The problem is that there is nothing being drawn to the Canvas when the drawFrame is called from handleNextFrame.
Where lies my problem, and how can I fix this issue?
Thanks in advance,
Jonas
I had the same issue.
Go to your project settings, select 'Build Settings' tab, and make sure 'Strict Concurrency Checking' is ON.
It will give you hints about GraphicsContext not being thread-safe, because it does not conform to Sendable protocol.
I am no expert on the topic, but I believe this means that GraphicsContext is not meant to be used in asynchronous context, you are not meant to save its reference for future use. You are given a GraphicsContext instance and you are meant to use it so long as the renderer closure's execution lasts (and this execution, of course, ends before your asynchronous callbacks could use the GraphicsContext instance).
What you really want is a TimelineView:
import SwiftUI
private func nanosValue(for date: Date) -> Int { return Calendar.current.component(.nanosecond, from: date)
}
struct ContentView: View {
var body: some View {
TimelineView(.periodic(from: Date(), by: 0.000001)) { timeContext in
Canvas { context, size in
let value = nanosValue(for: timeContext.date) % 2
let rect = CGRect(origin: .zero, size: size).insetBy(dx: 25, dy: 25)
// Path
let path = Path(roundedRect: rect, cornerRadius: 35.0)
// Gradient
let gradient = Gradient(colors: [.green, value == 1 ? .blue : .red])
let from = rect.origin
let to = CGPoint(x: rect.width + from.x, y: rect.height + from.y)
// Stroke path
context.stroke(path, with: .color(.blue), lineWidth: 25)
// Fill path
context.fill(path, with: .linearGradient(gradient,
startPoint: from,
endPoint: to))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This will change the gradient colour of the drawn rectangle every nanosecond.
Funny thing, I have heard on some blog, you need a completely different mindset to use SwiftUI, haha. It just does not work the traditional way.

Restarting perpetual animation after stopping in SwiftUI

Background
In this learning app, I've followed and excellent tutorial from Hacking with Swift on generating a wave-like animation. I've modified this app further adding some functionalities:
Providing Start/Stop mechanism for the wave animation
Perpetually generating random numbers for the duration of the animation
Modifying animation if an "interesting" number is found. Initially, I've implemented logic that defines even numbers as interesting but that could be easily changes to flag prime numbers, etc.
Problem
After stopping the animation does not "run" again. This is demonstrated in the gif below.
After stopping the animation does not restart.
Code
//
// ContentView.swift
// WaveExample
//
// Created by Konrad on 28/07/2021.
// Original tutorial: https://www.hackingwithswift.com/plus/custom-swiftui-components/creating-a-waveview-to-draw-smooth-waveforms
//
import SwiftUI
/**
Creates wave shape object
- Parameter strength: How tall the wave should be
- Parameter frequency: How densly the wave should be packed
- returns: Shape
*/
struct Wave: Shape {
// Basic wave characteristics
var strength: Double // Height
var frequency: Double // Number of hills
var phase: Double // Offsets the wave, can be used to animate the view
// Required to define that animation relates to moving the wave from left to right
var animatableData: Double {
get { phase }
set { self.phase = newValue }
}
// Path drawing function
func path(in rect: CGRect) -> Path {
let path = UIBezierPath()
// Basic waveline characteristics
let width = Double(rect.width)
let height = Double(rect.height)
let midWidth = width / 2
let midHeight = height / 2
let wavelength = width / frequency
let oneOverMidWidth = 1 / midWidth
// Path characteristics
path.move(to: CGPoint(x: 0, y: midHeight))
// By determines the nmber of calculations, can be decreased to run faster
for xPosition in stride(from: 0, through: width, by: 1) {
let relativeX = xPosition / wavelength // How far we are from the start point
let distanceFromMidWidth = xPosition - midWidth // Distance from the middle of the space
let normalDistance = distanceFromMidWidth * oneOverMidWidth // Get values from -1 to 1, normalize
// let parabola = normalDistance // Small waves in the middle
let parabola = -(normalDistance * normalDistance) + 1 // Big wave in the middle
let sine = sin(relativeX + phase) // Offset based on phase
let yPosition = parabola * strength * sine + midHeight // Moving this halfway
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
return Path(path.cgPath)
}
}
struct Line: Shape {
func path(in rect: CGRect) -> Path {
// Positioning
let midHeight = rect.height / 2
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: midHeight))
path.addLine(to: CGPoint(x: rect.width, y: midHeight))
return Path(path.cgPath)
}
}
struct ContentView: View {
#State private var phase = 0.0 // Used to animate the wave
#State private var waveStrength: Double = 10.0 // How tall, change for interesting numbers
#State private var waveFrequency: Double = 10.0 // How frequent, change for interesting numbers
#State var isAnimating: Bool = false // Currently running animation
#State private var randNum: Int16 = 0 // Random number to keep generating while animating
#State private var isNumberInteresting: Bool = false // Will take 'true' of the random number has some interesting properties
// Timer publisher reflecting frequent animation changes
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
// Stop timer
func stopTimer() {
self.timer.upstream.connect().cancel()
}
// Start timer
func startTimer() {
self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
}
// Check if number is interesting
func checkNumber(num: Int16) -> Bool {
var isInteresting: Bool = false
if num % 2 == 0 {
isInteresting.toggle()
}
return isInteresting
}
var body: some View {
VStack {
if self.isAnimating {
VStack {
Button("Stop") {
self.isAnimating = false
stopTimer()
}
.font(.title)
.foregroundColor(Color(.blue))
Text("Random number: \(String(randNum)), interesting: \(String(isNumberInteresting))")
.onReceive(timer, perform: { _ in
randNum = Int16.random(in: 0..<Int16.max)
isNumberInteresting = checkNumber(num: randNum)
})
}
} else {
Button("Start") {
self.isAnimating = true
startTimer()
}
.font(.title)
.foregroundColor(Color(.red))
}
if self.isAnimating {
// Animation
ZStack {
ForEach(0..<10) { waveIteration in
Wave(strength: waveStrength, frequency: waveFrequency, phase: phase)
.stroke(Color.blue.opacity(Double(waveIteration) / 3), lineWidth: 1.1)
.offset(y: CGFloat(waveIteration) * 10)
}
}
.onReceive(timer) { _ in
// withAnimation requires info on how to animate
withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) {
self.phase = .pi * 2 // 180 degrees of sine being calculated
if isNumberInteresting {
waveFrequency = 50.0
waveStrength = 50.0
} else {
waveFrequency = 10.0
waveStrength = 10.0
}
}
}
.frame(height: UIScreen.main.bounds.height * 0.8)
} else {
// Static line
ZStack {
Line()
.stroke(Color.blue)
}
.frame(height: UIScreen.main.bounds.height * 0.8)
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Side notes
In addiction to the problem above any good practice pointers on working with Swift are always welcome.
I made your project works, you can see the changed code // <<: Here!, the issue was there that you did not show the Animation the changed value! you showed just one time! and after that you keep it the same! if you see your code in your question you are repeating self.phase = .pi * 2 it makes no meaning to Animation! I just worked on your ContentView the all project needs refactor work, but that is not the issue here.
struct ContentView: View {
#State private var phase = 0.0
#State private var waveStrength: Double = 10.0
#State private var waveFrequency: Double = 10.0
#State var isAnimating: Bool = false
#State private var randNum: Int16 = 0
#State private var isNumberInteresting: Bool = false
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var stringOfText: String = String() // <<: Here!
func stopTimer() {
self.timer.upstream.connect().cancel()
phase = 0.0 // <<: Here!
}
func startTimer() {
self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(500)) { phase = .pi * 2 } // <<: Here!
}
func checkNumber(num: Int16) -> Bool {
var isInteresting: Bool = false
if num % 2 == 0 {
isInteresting.toggle()
}
return isInteresting
}
var body: some View {
VStack {
Button(isAnimating ? "Stop" : "Start") { // <<: Here!
isAnimating.toggle() // <<: Here!
isAnimating ? startTimer() : stopTimer() // <<: Here!
}
.font(.title)
.foregroundColor(isAnimating ? Color.red : Color.blue) // <<: Here!
ZStack {
if isAnimating {
ForEach(0..<10) { waveIteration in
Wave(strength: waveStrength, frequency: waveFrequency, phase: phase)
.stroke(Color.blue.opacity(Double(waveIteration) / 3), lineWidth: 1.1)
.offset(y: CGFloat(waveIteration) * 10)
}
}
else {
Line().stroke(Color.blue)
}
}
.frame(height: UIScreen.main.bounds.height * 0.8)
.overlay(isAnimating ? Text(stringOfText) : nil, alignment: .top) // <<: Here!
.onReceive(timer) { _ in
if isAnimating { // <<: Here!
randNum = Int16.random(in: 0..<Int16.max)
isNumberInteresting = checkNumber(num: randNum)
stringOfText = "Random number: \(String(randNum)), interesting: \(String(isNumberInteresting))" // <<: Here!
if isNumberInteresting {
waveFrequency = 50.0
waveStrength = 50.0
} else {
waveFrequency = 10.0
waveStrength = 10.0
}
}
else {
stopTimer() // For safety! Killing Timer in case! // <<: Here!
}
}
.animation(nil, value: stringOfText) // <<: Here!
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) // <<: Here!
.id(isAnimating) // <<: Here!
}
}
}

Why does a SwiftUI Rectangle with opacity 0.0 render a grey rectangle on a transparent NSWindow?

I have a transparent NSWindow containing a Grid of Rectangles whose opacity can be toggled. When the opacity is 0.0 a grey rectangle is rendered (see image) where I would expect to see an empty space.
I've tried modifying the background colour to Color.clear in all the parent views (littered throughout this code), but suspect the cause lies elsewhere - perhaps due to the way transparent windows behave, as there also appear to be drop shadows and outlines etc.
NOTE: Toggling between opacity 1.0 and 0.0 behaves slightly differently, so wondered if a view gets backed if the opacity is < 1.0 || > 0.0?
My target is macOS 11.1
My NSWindow is constructed thus:
class TransparentWindow: NSWindow {
init(contentRect: NSRect, backing: NSWindow.BackingStoreType, defer flag: Bool) {
super.init(contentRect: contentRect, styleMask: [.borderless, .fullSizeContentView], backing: .buffered, defer: false)
ignoresMouseEvents = true
titlebarAppearsTransparent = true
titleVisibility = .hidden
isOpaque = false
backgroundColor = NSColor.clear
level = .floating
isMovable = false
isReleasedWhenClosed = false
orderFront(nil)
toolbar?.isVisible = false
// Hide the traffic icons (standard close, minimize, maximize buttons)
standardWindowButton(.closeButton)?.isHidden = true
standardWindowButton(.miniaturizeButton)?.isHidden = true
standardWindowButton(.zoomButton)?.isHidden = true
styleMask.insert(NSWindow.StyleMask.fullSizeContentView)
}
}
And views are roughly as follows:
struct GridView : View {
#EnvironmentObject var appStore : AppStore
var body: some View {
let columns: [GridItem] =
Array(repeating: .init(.fixed(size), spacing: spacing), count: cols)
LazyVGrid(columns: columns, spacing: spacing) {
ForEach(appStore.items) { item in
GridItemView(item: item)
.frame(width: size, height: size)
.background(Color.clear)
.border(Color.clear, width: 0.0)
}
}.padding(0)
.fixedSize()
.background(Color.clear)
}
}
struct GridItemView : View {
#EnvironmentObject var appStore : AppStore
#ObservedObject var item: Item
#State var opacity = 0.5
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.white)
.onReceive(appStore.$frame) { frame in
let newFrame = calculateItemRect(
frame: frame,
crop: geometry.frame(in: .global)
)
if (!newFrame.equalTo(item.frame)) {
item.frame = newFrame
}
}.onReceive(item.$toggle) { toggle in
if toggle {
opacity = 0.0
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
opacity = 0.5
item.toggle = false
}
}
}.opacity(opacity)
}.background(Color.clear)
}
}
Turns out I needed to set the hasShadow property to false on the NSWindow instance:
class TransparentWindow: NSWindow {
// ...
hasShadow = false
ignoresMouseEvents = true
// ...
}
}

Swiftui getting an image's displaying dimensions

I'm trying to get the dimensions of a displayed image to draw bounding boxes over the text I have recognized using apple's Vision framework.
So I run the VNRecognizeTextRequest uppon the press of a button with this funcion
func readImage(image:NSImage, completionHandler:#escaping(([VNRecognizedText]?,Error?)->()), comp:#escaping((Double?,Error?)->())) {
var recognizedTexts = [VNRecognizedText]()
var rr = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let requestHandler = VNImageRequestHandler(cgImage: image.cgImage(forProposedRect: &rr, context: nil, hints: nil)!
, options: [:])
let textRequest = VNRecognizeTextRequest { (request, error) in
guard let observations = request.results as? [VNRecognizedTextObservation] else { completionHandler(nil,error)
return
}
for currentObservation in observations {
let topCandidate = currentObservation.topCandidates(1)
if let recognizedText = topCandidate.first {
recognizedTexts.append(recognizedText)
}
}
completionHandler(recognizedTexts,nil)
}
textRequest.recognitionLevel = .accurate
textRequest.recognitionLanguages = ["es"]
textRequest.usesLanguageCorrection = true
textRequest.progressHandler = {(request, value, error) in
comp(value,nil)
}
try? requestHandler.perform([textRequest])
}
and compute the bounding boxes offsets using this struct and function
struct DisplayingRect:Identifiable {
var id = UUID()
var width:CGFloat = 0
var height:CGFloat = 0
var xAxis:CGFloat = 0
var yAxis:CGFloat = 0
init(width:CGFloat, height:CGFloat, xAxis:CGFloat, yAxis:CGFloat) {
self.width = width
self.height = height
self.xAxis = xAxis
self.yAxis = yAxis
}
}
func createBoundingBoxOffSet(recognizedTexts:[VNRecognizedText], image:NSImage) -> [DisplayingRect] {
var rects = [DisplayingRect]()
let imageSize = image.size
let imageTransform = CGAffineTransform.identity.scaledBy(x: imageSize.width, y: imageSize.height)
for obs in recognizedTexts {
let observationBounds = try? obs.boundingBox(for: obs.string.startIndex..<obs.string.endIndex)
let rectangle = observationBounds?.boundingBox.applying(imageTransform)
print("Rectange: \(rectangle!)")
let width = rectangle!.width
let height = rectangle!.height
let xAxis = rectangle!.origin.x - imageSize.width / 2 + rectangle!.width / 2
let yAxis = -(rectangle!.origin.y - imageSize.height / 2 + rectangle!.height / 2)
let rect = DisplayingRect(width: width, height: height, xAxis: xAxis, yAxis: yAxis)
rects.append(rect)
}
return(rects)
}
I place the rects using this code in the ContentView
ZStack{
Image(nsImage: self.img!)
.scaledToFit()
ForEach(self.rects) { rect in
Rectangle()
.fill(Color.init(.sRGB, red: 1, green: 0, blue: 0, opacity: 0.2))
.frame(width: rect.width, height: rect.height)
.offset(x: rect.xAxis, y: rect.yAxis)
}
}
If I use the original's image dimensions I get these results
But if I add
Image(nsImage: self.img!)
.resizable()
.scaledToFit()
I get these results
Is there a way to get the image dimensions and pass them and get the proper size of the image being displayed? I also need this because I can't show the whole image sometimes and need to scale it.
Thanks a lot
I would use GeometryReader on background so it reads exactly size of image, as below
#State var imageSize: CGSize = .zero // << or initial from NSImage
...
Image(nsImage: self.img!)
.resizable()
.scaledToFit()
.background(rectReader())
// ... somewhere below
private func rectReader() -> some View {
return GeometryReader { (geometry) -> Color in
let imageSize = geometry.size
DispatchQueue.main.async {
print(">> \(imageSize)") // use image actual size in your calculations
self.imageSize = imageSize
}
return .clear
}
}
Rather than pass in the frame to every view, Apple elected to give you a separate GeometryReader view that gets its frame passed in as a parameter to its child closure.
struct Example: View {
var body: some View {
GeometryReader { geometry in
Image(systemName: "check")
.onAppear {
print(geometry.frame(in: .local))
}
}
}
}