If you take a snapshot after moving in swiftUI, the initial screen appears.
The code used is below.
//struct EidtBoxView: View {
#State private var location: CGPoint = CGPoint(x: UIScreen_width/2, y: UIScreen_width/2)
#GestureState private var fingerLocation: CGPoint? = nil
#GestureState private var startLocation: CGPoint? = nil // 1
#State var scale: CGFloat = 1.0
#State var lastScaleValue: CGFloat = 1.0
var simpleDrag: some Gesture {
DragGesture()
.onChanged { value in
var newLocation = startLocation ?? location // 3
newLocation.x += value.translation.width
newLocation.y += value.translation.height
self.location = newLocation
}.updating($startLocation) { (value, startLocation, transaction) in
startLocation = startLocation ?? location // 2
}
}
var fingerDrag: some Gesture {
DragGesture()
.updating($fingerLocation) { (value, fingerLocation, transaction) in
fingerLocation = value.location
}
}
var magnification: some Gesture {
MagnificationGesture().onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
self.scale = self.scale * delta
}.onEnded { _ in
self.lastScaleValue = 1.0
}
}
var body: some View {
RoundedRectangle(cornerRadius: 0)
.frame(width: UIScreen_width, height: UIScreen_width) // , alignment: .top)
.overlay(
ZStack{
// background image
Image(uiImage: UIImage(data: (selectPhoto.data)!)!)
.resizable()
.aspectRatio(1, contentMode: .fill)
// effect image
Image(uiImage: UIImage(named: "t2_big_01.png")!)
.resizable()
.aspectRatio(1, contentMode: .fill)
.frame(width: Effect_box_size, height: Effect_box_size)
.position(location)
.gesture(
simpleDrag.simultaneously(with: fingerDrag)
)
.scaleEffect(self.scale)
.gesture(magnification)
}
)
.clipped()
}
//}
//extension View {
func snapshot() -> UIImage {
let controller = UIHostingController(rootView: self.edgesIgnoringSafeArea(.all))
let view = controller.view
let targetSize = controller.view.intrinsicContentSize
view?.bounds = CGRect(origin: .zero, size: targetSize)
view?.backgroundColor = .clear
let renderer = UIGraphicsImageRenderer(size: targetSize)
return renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}
}
func asImage() -> UIImage {
let controller = UIHostingController(rootView: self)
let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)
let image = controller.view.asImage()
return image
}
//}
//extension UIView {
func asImage() -> UIImage {
let format = UIGraphicsImageRendererFormat()
format.scale = 1
return UIGraphicsImageRenderer(size: self.layer.frame.size, format: format).image { context in
self.drawHierarchy(in: self.layer.bounds, afterScreenUpdates: true)
}
}
//}
savedImage = EidtBoxView().snapshot()
savedImage = EidtBoxView().asImage()
Is there a way to make the image appear after moving?
I hope it goes well
How to build a page that works exactly like the Photo Apps of the iOS that can zoom into a photo using MagnificationGesture() and can pan after zoom using Pure SwiftUI?
I have tried to look for solutions in the forum, yet, none of question has a solution yet. Any advise?
Here is my code:
let magnificationGesture = MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
}
.onEnded { amount in
self.finalAmount += self.currentAmount
self.currentAmount = 0
}
let tapGesture = TapGesture()
.onEnded {
self.currentAmount = 0
self.finalAmount = 1
}
Image("Cat")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:UIScreen.main.bounds.width,height: UIScreen.main.bounds.height)
.scaleEffect(finalAmount + currentAmount)
.simultaneousGesture(magnificationGesture)
.simultaneousGesture(tapGesture)
Originally, I tried to add 1 more simultaneousGesture, dragGesture() which adjust the offset, but it fails to work.
The current code zoom the image well, but after zoom in, I want it to be allowed to pan. I have tried to add UIScrollView it also fails.
Here is my thought:
let dragGesture = DragGesture()
.onChanged { value in self.offset = value.translation }
and to add .offset() to the image.
However, it fails to work and the simulator is out of memory.
Any advise?
Use DragGesture() with position.
Tested Xcode 12.1 with iOS 14.2 (No Memory issues.)
struct ContentView: View {
#State private var currentAmount: CGFloat = 0
#State private var finalAmount: CGFloat = 1
#State private var location: CGPoint = CGPoint(x: UIScreen.main.bounds.width/2, y: UIScreen.main.bounds.height/2)
#GestureState private var startLocation: CGPoint? = nil
var body: some View {
let magnificationGesture = MagnificationGesture()
.onChanged { amount in
self.currentAmount = amount - 1
}
.onEnded { amount in
self.finalAmount += self.currentAmount
self.currentAmount = 0
}
// Here is create DragGesture and handel jump when you again start the dragging/
let dragGesture = DragGesture()
.onChanged { value in
var newLocation = startLocation ?? location
newLocation.x += value.translation.width
newLocation.y += value.translation.height
self.location = newLocation
}.updating($startLocation) { (value, startLocation, transaction) in
startLocation = startLocation ?? location
}
return Image("temp_1")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
.scaleEffect(finalAmount + currentAmount)
.position(location)
.gesture(
dragGesture.simultaneously(with: magnificationGesture)
)
}
}
I finally managed to solve the issue with UIViewRepresentable.
struct ImageScrollView: UIViewRepresentable {
private var contentSizeWidth: CGFloat = 0
private var contentSizeHeight: CGFloat = 0
private var imageView = UIImageView()
private var scrollView = UIScrollView()
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<ImageScrollView>) -> UIScrollView {
let image = UIImage(named: "Dummy")
let width = image?.size.width
let height = image?.size.height
imageView.image = image
imageView.frame = CGRect(x: 0, y: 0, width: width ?? 0, height: height ?? 0)
imageView.contentMode = UIView.ContentMode.scaleAspectFit
imageView.isUserInteractionEnabled = true
scrollView.delegate = context.coordinator
scrollView.isScrollEnabled = true
scrollView.clipsToBounds = true
scrollView.bouncesZoom = true
scrollView.isUserInteractionEnabled = true
scrollView.minimumZoomScale = 0.5 //scrollView.frame.size.width / (width ?? 1)
scrollView.maximumZoomScale = 2
scrollView.zoomScale = 1
scrollView.contentSize = imageView.frame.size
scrollView.addSubview(imageView)
let doubleTapGestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(sender:)))
doubleTapGestureRecognizer.numberOfTapsRequired = 2;
doubleTapGestureRecognizer.numberOfTouchesRequired=1;
scrollView.addGestureRecognizer(doubleTapGestureRecognizer)
imageView.addGestureRecognizer(doubleTapGestureRecognizer)
return scrollView
}
func updateUIView(_ uiView: UIScrollView,
context: UIViewRepresentableContext<ImageScrollView>) {
}
class Coordinator: NSObject, UIScrollViewDelegate {
var control: ImageScrollView
init(_ control: ImageScrollView) {
self.control = control
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("scrollViewDidScroll")
centerImage()
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView,
with view: UIView?,
atScale scale: CGFloat) {
print("scrollViewDidEndZooming")
print(scale, scrollView.minimumZoomScale, scrollView.maximumZoomScale)
scrollView.setZoomScale(scale, animated: true)
centerImage()
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.control.imageView
}
func centerImage() {
let boundSize = self.control.scrollView.frame.size
var frameToCenter = self.control.imageView.frame
frameToCenter.origin.x = 0
frameToCenter.origin.y = 0
if (frameToCenter.size.width<boundSize.width) {
frameToCenter.origin.x = (boundSize.width-frameToCenter.size.width)/2;
}
if (frameToCenter.size.height<boundSize.height) {
frameToCenter.origin.y = (boundSize.height-frameToCenter.size.height)/2;
}
self.control.imageView.frame = frameToCenter
}
#objc func handleTap(sender: UITapGestureRecognizer) {
if (self.control.scrollView.zoomScale==self.control.scrollView.minimumZoomScale) {
self.control.scrollView.zoomScale = self.control.scrollView.maximumZoomScale/2;
} else {
self.control.scrollView.zoomScale = self.control.scrollView.minimumZoomScale;
}
print("tap")
}
}
}
I am currently trying to achieve to draw boxes of the text that was recognized with Firebase ML Kit on top of the image.
Currently, I did not have success yet and I can't see any box at all as they are all shown offscreen. I was looking at this article for a reference: https://medium.com/swlh/how-to-draw-bounding-boxes-with-swiftui-d93d1414eb00 and also at that project: https://github.com/firebase/quickstart-ios/blob/master/mlvision/MLVisionExample/ViewController.swift
This is the view where the boxes should be shown:
struct ImageScanned: View {
var image: UIImage
#Binding var rectangles: [CGRect]
#State var viewSize: CGSize = .zero
var body: some View {
// TODO: fix scaling
ZStack {
Image(uiImage: image)
.resizable()
.scaledToFit()
.overlay(
GeometryReader { geometry in
ZStack {
ForEach(self.transformRectangles(geometry: geometry)) { rect in
Rectangle()
.path(in: CGRect(
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height))
.stroke(Color.red, lineWidth: 2.0)
}
}
}
)
}
}
private func transformRectangles(geometry: GeometryProxy) -> [DetectedRectangle] {
var rectangles: [DetectedRectangle] = []
let imageViewWidth = geometry.frame(in: .global).size.width
let imageViewHeight = geometry.frame(in: .global).size.height
let imageWidth = image.size.width
let imageHeight = image.size.height
let imageViewAspectRatio = imageViewWidth / imageViewHeight
let imageAspectRatio = imageWidth / imageHeight
let scale = (imageViewAspectRatio > imageAspectRatio)
? imageViewHeight / imageHeight : imageViewWidth / imageWidth
let scaledImageWidth = imageWidth * scale
let scaledImageHeight = imageHeight * scale
let xValue = (imageViewWidth - scaledImageWidth) / CGFloat(2.0)
let yValue = (imageViewHeight - scaledImageHeight) / CGFloat(2.0)
var transform = CGAffineTransform.identity.translatedBy(x: xValue, y: yValue)
transform = transform.scaledBy(x: scale, y: scale)
for rect in self.rectangles {
let rectangle = rect.applying(transform)
rectangles.append(DetectedRectangle(width: rectangle.width, height: rectangle.height, x: rectangle.minX, y: rectangle.minY))
}
return rectangles
}
}
struct DetectedRectangle: Identifiable {
var id = UUID()
var width: CGFloat = 0
var height: CGFloat = 0
var x: CGFloat = 0
var y: CGFloat = 0
}
This is the view where this view is nested in:
struct StartScanView: View {
#State var showCaptureImageView: Bool = false
#State var image: UIImage? = nil
#State var rectangles: [CGRect] = []
var body: some View {
ZStack {
if showCaptureImageView {
CaptureImageView(isShown: $showCaptureImageView, image: $image)
} else {
VStack {
Button(action: {
self.showCaptureImageView.toggle()
}) {
Text("Start Scanning")
}
// show here View with rectangles on top of image
if self.image != nil {
ImageScanned(image: self.image ?? UIImage(), rectangles: $rectangles)
}
Button(action: {
self.processImage()
}) {
Text("Process Image")
}
}
}
}
}
func processImage() {
let scaledImageProcessor = ScaledElementProcessor()
if image != nil {
scaledImageProcessor.process(in: image!) { text in
for block in text.blocks {
for line in block.lines {
for element in line.elements {
self.rectangles.append(element.frame)
}
}
}
}
}
}
}
The calculation of the tutorial caused the rectangles being to big and the one of the sample project them being too small.
(Similar for height)
Unfortunately I can't find on which size Firebase determines the element's size.
This is how it looks like:
Without calculating the width & height at all, the rectangles seem to have about the size they are supposed to have (not exactly), so this gives me the assumption, that ML Kit's size calculation is not done in proportion to the image.size.height/width.
This is how i changed the foreach loop
Image(uiImage: uiimage!).resizable().scaledToFit().overlay(
GeometryReader{ (geometry: GeometryProxy) in
ForEach(self.blocks , id: \.self){ (block:VisionTextBlock) in
Rectangle().path(in: block.frame.applying(self.transformMatrix(geometry: geometry, image: self.uiimage!))).stroke(Color.purple, lineWidth: 2.0)
}
}
)
Instead of passing the x, y, width and height, I am passing the return value from transformMatrix function to the path function.
My transformMatrix function is
private func transformMatrix(geometry:GeometryProxy, image:UIImage) -> CGAffineTransform {
let imageViewWidth = geometry.size.width
let imageViewHeight = geometry.size.height
let imageWidth = image.size.width
let imageHeight = image.size.height
let imageViewAspectRatio = imageViewWidth / imageViewHeight
let imageAspectRatio = imageWidth / imageHeight
let scale = (imageViewAspectRatio > imageAspectRatio) ?
imageViewHeight / imageHeight :
imageViewWidth / imageWidth
// Image view's `contentMode` is `scaleAspectFit`, which scales the image to fit the size of the
// image view by maintaining the aspect ratio. Multiple by `scale` to get image's original size.
let scaledImageWidth = imageWidth * scale
let scaledImageHeight = imageHeight * scale
let xValue = (imageViewWidth - scaledImageWidth) / CGFloat(2.0)
let yValue = (imageViewHeight - scaledImageHeight) / CGFloat(2.0)
var transform = CGAffineTransform.identity.translatedBy(x: xValue, y: yValue)
transform = transform.scaledBy(x: scale, y: scale)
return transform
}
}
and the output is
ML Kit has a QuickStart app showing exactly what you are trying to do: recognizing the text and drawing a rectangle around the text. Here is the Swift code:
https://github.com/firebase/quickstart-ios/tree/master/mlvision/MLVisionExample
Hi I am new to swift 3 and Xcode 8 I have an issue in my below code Build get succeeded and it runs well in a simulator but image and text are not get showed what I did wrong?
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
generateImageWithText(text: "HelloWorld")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#discardableResult
func generateImageWithText(text: String) -> UIImage
{
let image = UIImage(named: "apple_led.png")!
let imageView = UIImageView(image: image)
imageView.backgroundColor = UIColor.black
imageView.frame = CGRect(origin: CGPoint(x: 0,y :0), size: CGSize(width: 100, height: 100))
let label = UILabel(frame: CGRect(origin: CGPoint(x: 0,y :0), size: CGSize(width: 50, height: 50)))
label.backgroundColor = UIColor.white
label.textAlignment = .center
label.textColor = UIColor.blue
label.text = text
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0);
imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
label.layer.render(in: UIGraphicsGetCurrentContext()!)
let imageWithText = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
print(image)
print(text)
print(label)
return imageWithText!
}
}
Try this
func mergeImageAndText(text: NSString, atPoint: CGPoint) -> UIImage{
let demoImage = UIImage(named: "test.png")!
// Setup the font specific variables
let textColor = UIColor.white
let textFont = UIFont(name: "Helvetica", size: 14)!
// Setup the image context using the passed image
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(demoImage.size, false, scale)
// Setup the font attributes that will be later used to dictate how the text should be drawn
let textFontAttributes = [
NSFontAttributeName: textFont,
NSForegroundColorAttributeName: textColor,
] as [String : Any]
demoImage.draw(in: CGRect(x: 0, y: 0, width: 50, height:50))
let rect = CGRect(x: atPoint.x, y: atPoint.y, width: demoImage.size.width, height: demoImage.size.height)
// Draw the text into an image
text.draw(in: rect, withAttributes: textFontAttributes)
// Create a new image out of the images we have created
let newImage = UIGraphicsGetImageFromCurrentImageContext()
// End the context now that we have the image we need
UIGraphicsEndImageContext()
//Pass the image back up to the caller
return newImage!
}
Method Call:
let imagenew = self.mergeImageAndText(text: "TEST", atPoint: CGPoint(x: 10, y: 10))
I'm trying to make a graphing calculator app and I can't get AxesDrawer to work. This is from the stanford university course with swift 2 and I don't know how to draw this out using UIBezierPath etc
AxesDrawer.swift:
import UIKit
class AxesDrawer
{
private struct Constants {
static let HashmarkSize: CGFloat = 6
}
var color = UIColor.blue
var minimumPointsPerHashmark: CGFloat = 40
var contentScaleFactor: CGFloat = 1 // set this from UIView's contentScaleFactor to position axes with maximum accuracy
convenience init(color: UIColor, contentScaleFactor: CGFloat) {
self.init()
self.color = color
self.contentScaleFactor = contentScaleFactor
}
convenience init(color: UIColor) {
self.init()
self.color = color
}
convenience init(contentScaleFactor: CGFloat) {
self.init()
self.contentScaleFactor = contentScaleFactor
}
// this method is the heart of the AxesDrawer
// it draws in the current graphic context's coordinate system
// therefore origin and bounds must be in the current graphics context's coordinate system
// pointsPerUnit is essentially the "scale" of the axes
// e.g. if you wanted there to be 100 points along an axis between -1 and 1,
// you'd set pointsPerUnit to 50
func drawAxesInRect(bounds: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
{
UIGraphicsGetCurrentContext()!.saveGState()
color.set()
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.minX, y: align(coordinate: origin.y)))
path.addLine(to: CGPoint(x: bounds.maxX, y: align(coordinate: origin.y)))
path.move(to: CGPoint(x: align(coordinate: origin.x), y: bounds.minY))
path.addLine(to: CGPoint(x: align(coordinate: origin.x), y: bounds.maxY))
path.stroke()
drawHashmarksInRect(bounds: bounds, origin: origin, pointsPerUnit: abs(pointsPerUnit))
UIGraphicsGetCurrentContext()!.restoreGState()
}
// the rest of this class is private
private func drawHashmarksInRect(bounds: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
{
if ((origin.x >= bounds.minX) && (origin.x <= bounds.maxX)) || ((origin.y >= bounds.minY) && (origin.y <= bounds.maxY))
{
// figure out how many units each hashmark must represent
// to respect both pointsPerUnit and minimumPointsPerHashmark
var unitsPerHashmark = minimumPointsPerHashmark / pointsPerUnit
if unitsPerHashmark < 1 {
unitsPerHashmark = pow(10, ceil(log10(unitsPerHashmark)))
} else {
unitsPerHashmark = floor(unitsPerHashmark)
}
let pointsPerHashmark = pointsPerUnit * unitsPerHashmark
// figure out which is the closest set of hashmarks (radiating out from the origin) that are in bounds
var startingHashmarkRadius: CGFloat = 1
if !bounds.contains(origin) {
let leftx = max(origin.x - bounds.maxX, 0)
let rightx = max(bounds.minX - origin.x, 0)
let downy = max(origin.y - bounds.minY, 0)
let upy = max(bounds.maxY - origin.y, 0)
startingHashmarkRadius = min(min(leftx, rightx), min(downy, upy)) / pointsPerHashmark + 1
}
// now create a bounding box inside whose edges those four hashmarks lie
let bboxSize = pointsPerHashmark * startingHashmarkRadius * 2
var bbox = CGRect(center: origin, size: CGSize(width: bboxSize, height: bboxSize))
// formatter for the hashmark labels
let formatter = NumberFormatter()
formatter.maximumFractionDigits = Int(-log10(Double(unitsPerHashmark)))
formatter.minimumIntegerDigits = 1
// radiate the bbox out until the hashmarks are further out than the bounds
while !bbox.contains(bounds)
{
let label = formatter.string(from: NSNumber(value: Int(origin.x-bbox.minX / pointsPerUnit)))
if let leftHashmarkPoint = alignedPoint(x: bbox.minX, y: origin.y, insideBounds:bounds) {
drawHashmarkAtLocation(location: leftHashmarkPoint, .Top("-\(label)"))
}
if let rightHashmarkPoint = alignedPoint(x: bbox.maxX, y: origin.y, insideBounds:bounds) {
drawHashmarkAtLocation(location: rightHashmarkPoint, AnchoredText.Top(label!))
}
if let topHashmarkPoint = alignedPoint(x: origin.x, y: bbox.minY, insideBounds:bounds) {
drawHashmarkAtLocation(location: topHashmarkPoint, AnchoredText.Left(label!))
}
if let bottomHashmarkPoint = alignedPoint(x: origin.x, y: bbox.maxY, insideBounds:bounds) {
drawHashmarkAtLocation(location: bottomHashmarkPoint, .Left("-\(label)"))
}
bbox.insetBy(dx: -pointsPerHashmark, dy: -pointsPerHashmark)
}
}
}
private func drawHashmarkAtLocation(location: CGPoint, _ text: AnchoredText)
{
var dx: CGFloat = 0, dy: CGFloat = 0
switch text {
case .Left: dx = Constants.HashmarkSize / 2
case .Right: dx = Constants.HashmarkSize / 2
case .Top: dy = Constants.HashmarkSize / 2
case .Bottom: dy = Constants.HashmarkSize / 2
}
let path = UIBezierPath()
path.move(to: CGPoint(x: location.x-dx, y: location.y-dy))
path.addLine(to: CGPoint(x: location.x+dx, y: location.y+dy))
path.stroke()
text.drawAnchoredToPoint(location: location, color: color)
}
private enum AnchoredText
{
case Left(String)
case Right(String)
case Top(String)
case Bottom(String)
static let VerticalOffset: CGFloat = 3
static let HorizontalOffset: CGFloat = 6
func drawAnchoredToPoint(location: CGPoint, color: UIColor) {
let attributes = [
NSFontAttributeName : UIFont.preferredFont(forTextStyle: UIFontTextStyle.footnote),
NSForegroundColorAttributeName : color
]
var textRect = CGRect(center: location, size: text.size(attributes: attributes))
switch self {
case .Top: textRect.origin.y += textRect.size.height / 2 + AnchoredText.VerticalOffset
case .Left: textRect.origin.x += textRect.size.width / 2 + AnchoredText.HorizontalOffset
case .Bottom: textRect.origin.y -= textRect.size.height / 2 + AnchoredText.VerticalOffset
case .Right: textRect.origin.x -= textRect.size.width / 2 + AnchoredText.HorizontalOffset
}
text.draw(in: textRect, withAttributes: attributes)
}
var text: String {
switch self {
case .Left(let text): return text
case .Right(let text): return text
case .Top(let text): return text
case .Bottom(let text): return text
}
}
}
// we want the axes and hashmarks to be exactly on pixel boundaries so they look sharp
// setting contentScaleFactor properly will enable us to put things on the closest pixel boundary
// if contentScaleFactor is left to its default (1), then things will be on the nearest "point" boundary instead
// the lines will still be sharp in that case, but might be a pixel (or more theoretically) off of where they should be
private func alignedPoint(x x: CGFloat, y: CGFloat, insideBounds: CGRect? = nil) -> CGPoint?
{
let point = CGPoint(x: align(coordinate: x), y: align(coordinate: y))
if let permissibleBounds = insideBounds, !permissibleBounds.contains(point) {
return nil
}
return point
}
private func align(coordinate: CGFloat) -> CGFloat {
return round(coordinate * contentScaleFactor) / contentScaleFactor
}
}
extension CGRect
{
init(center: CGPoint, size: CGSize) {
self.init(x: center.x-size.width/2, y: center.y-size.height/2, width: size.width, height: size.height)
}
}
ViewController.swift:
import UIKit
var calculatorCount = 0
class CalculatorViewController: UIViewController {
var graphl = GraphView()
private var on = true
#IBOutlet private var display: UILabel!
private var userIsInTheMiddleOfTyping = false
override func viewDidLoad() {
super.viewDidLoad()
calculatorCount += 1
//print("Loaded up a new Calculator (count = \(calculatorCount))")
brain.addUnaryOperation(symbol: "Z") { [ weak weakSelf = self ] in
weakSelf?.display.textColor = UIColor.red
return sqrt($0)
}
graphl.print2()
}
deinit {
calculatorCount -= 1
//print(" Calculator left the heap (count = \(calculatorCount))")
}
#IBAction func off(_ sender: UIButton) {
on = false
}
#IBAction func on(_ sender: UIButton) {
on = true
}
#IBAction private func tocuhDigit(_ sender: UIButton) {
if on {
let digit = sender.currentTitle!
if userIsInTheMiddleOfTyping {
let textCurrentlyInDisplay = display.text!
display.text = textCurrentlyInDisplay + digit
} else {
display.text = digit
}
userIsInTheMiddleOfTyping = true
}
}
private var displayValue: Double {
get {
return Double(display.text!)!
}
set {
display.text = String(newValue)
}
}
var savedProgram: CalculatorBrain.PropertyList?
#IBAction func save() {
savedProgram = brain.program
}
#IBAction func restore() {
if savedProgram != nil {
brain.program = savedProgram!
displayValue = brain.result
}
}
private var brain = CalculatorBrain()
#IBAction func Reset(_ sender: UIButton) {
if on {
displayValue = 0
}
}
#IBAction private func performOperation(_ sender: UIButton) {
if userIsInTheMiddleOfTyping && on {
brain.setOperand(operand: displayValue)
userIsInTheMiddleOfTyping = false
}
if let mathematicalSymbol = sender.currentTitle {
brain.perofrmOperation(symbol: mathematicalSymbol)
}
displayValue = brain.result
}
}
CalculatorBrain.swift:
import Foundation
class CalculatorBrain {
private var accumulator = 0.0
private var internalProgram = [AnyObject]()
func setOperand(operand: Double) {
accumulator = operand
internalProgram.append(operand as AnyObject)
}
func addUnaryOperation(symbol: String, operation: #escaping (Double) -> Double) {
operations[symbol] = Operation.UnaryOperation(operation)
}
private var operations: Dictionary<String, Operation> = [
"π" :Operation.Constant(M_PI),
"e" : Operation.Constant(M_E),
"±" : Operation.UnaryOperation({ -$0 }),
"∓" : Operation.UnaryOperation({+$0}),
"√" : Operation.UnaryOperation(sqrt), //sqrt,
"cos" : Operation.UnaryOperation(cos),
"×" : Operation.BinaryOperation({ $0 * $1 }),
"-" : Operation.BinaryOperation({ $0 - $1 }),
"+" : Operation.BinaryOperation({ $0 + $1 }),
"÷" : Operation.BinaryOperation({ $0 / $1 }),
"=" : Operation.Equals,
"i" : Operation.Constant(sqrt(-1)),
"x2" : Operation.UnaryOperation({$0 * $0}),
"xb" : Operation.BinaryOperation2({pow($0, $1)})
]
private enum Operation {
case Constant(Double)
case UnaryOperation((Double) -> Double)
case BinaryOperation((Double, Double) -> Double)
case Equals
case BinaryOperation2((Double, Double) -> Double)
}
func perofrmOperation(symbol: String) {
internalProgram.append(symbol as AnyObject)
if let operation = operations[symbol] {
switch operation {
case .Constant(let value): accumulator = value
case .UnaryOperation(let function): accumulator = function(accumulator)
case .BinaryOperation(let function): executePendingBinaryOperation()
pending = PendingBinaryOperationInfo(binaryFunction: function, firstOperand: accumulator)
case .Equals:
executePendingBinaryOperation()
case .BinaryOperation2(let function):
pending = PendingBinaryOperationInfo(binaryFunction: function, firstOperand: accumulator)
}
}
}
private func executePendingBinaryOperation() {
if pending != nil {
accumulator = pending!.binaryFunction(pending!.firstOperand, accumulator)
}
}
private var pending: PendingBinaryOperationInfo?
private struct PendingBinaryOperationInfo {
var binaryFunction: (Double, Double) -> Double
var firstOperand: Double
}
typealias PropertyList = AnyObject
var program: PropertyList {
get {
return internalProgram as CalculatorBrain.PropertyList
}
set {
clear()
if let arrayOfOps = newValue as? [AnyObject] {
for op in arrayOfOps {
if let operand = op as? Double {
setOperand(operand: operand)
} else if let operation = op as? String {
perofrmOperation(symbol: operation)
}
}
}
}
}
func clear() {
accumulator = 0.0
pending = nil
internalProgram.removeAll()
}
var result: Double {
get {
return accumulator
}
}
}
AppDelegate.swift:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
Main.storyboard:
[buttons are in stack a stack view and created a UIView for the graph][1]
[1]: https://i.stack.imgur.com/oxwmw.png
I know there is a lot of time since the question has been made, but I made the same question today and I hope this answer can help others:
You can follow these steps:
Create a new project on Xcode.
Add "AxesDrawer.swift" file to your project (File -> Add Files to "ProjectName"...
Create a new Cocoa Touch Class file using subclass UIView on the main folder of your project.
Add a View on Main.storyboard and set the Class Name of this view equal to the file created above.
Use the following code to override draw function in file created on step 3:
override func draw(_ rect: CGRect) {
//Draw axes
let axes: AxesDrawer = AxesDrawer.init(color: UIColor.black, contentScaleFactor: CGFloat(1))
axes.drawAxes(in: CGRect(origin: CGPoint(x: bounds.midX, y: bounds.midY),
size: CGSize(width: 1000, height: -1000)),
origin: CGPoint(x: bounds.midX, y: bounds.midY),
pointsPerUnit: CGFloat(2))
axes.drawAxes(in: CGRect(origin: CGPoint(x: bounds.midX, y: bounds.midY),
size: CGSize(width: -1000, height: 1000)),
origin: CGPoint(x: bounds.midX, y: bounds.midY),
pointsPerUnit: CGFloat(2))
//End Draw axes
}
//
// AxesDrawer.swift
// Calculator
//
// Created by CS193p Instructor.
// Copyright © 2015-17 Stanford University.
// All rights reserved.
//
import UIKit
struct AxesDrawer
{
var color: UIColor
var contentScaleFactor: CGFloat // set this from UIView's contentScaleFactor to position axes with maximum accuracy
var minimumPointsPerHashmark: CGFloat = 40 // public even though init doesn't accommodate setting it (it's rare to want to change it)
init(color: UIColor = UIColor.blue, contentScaleFactor: CGFloat = 1) {
self.color = color
self.contentScaleFactor = contentScaleFactor
}
// this method is the heart of the AxesDrawer
// it draws in the current graphic context's coordinate system
// therefore origin and bounds must be in the current graphics context's coordinate system
// pointsPerUnit is essentially the "scale" of the axes
// e.g. if you wanted there to be 100 points along an axis between -1 and 1,
// you'd set pointsPerUnit to 50
func drawAxes(in rect: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
{
UIGraphicsGetCurrentContext()?.saveGState()
color.set()
let path = UIBezierPath()
path.move(to: CGPoint(x: rect.minX, y: origin.y).aligned(usingScaleFactor: contentScaleFactor)!)
path.addLine(to: CGPoint(x: rect.maxX, y: origin.y).aligned(usingScaleFactor: contentScaleFactor)!)
path.move(to: CGPoint(x: origin.x, y: rect.minY).aligned(usingScaleFactor: contentScaleFactor)!)
path.addLine(to: CGPoint(x: origin.x, y: rect.maxY).aligned(usingScaleFactor: contentScaleFactor)!)
path.stroke()
drawHashmarks(in: rect, origin: origin, pointsPerUnit: abs(pointsPerUnit))
UIGraphicsGetCurrentContext()?.restoreGState()
}
// the rest of this class is private
private struct Constants {
static let hashmarkSize: CGFloat = 6
}
private let formatter = NumberFormatter() // formatter for the hashmark labels
private func drawHashmarks(in rect: CGRect, origin: CGPoint, pointsPerUnit: CGFloat)
{
if ((origin.x >= rect.minX) && (origin.x <= rect.maxX)) || ((origin.y >= rect.minY) && (origin.y <= rect.maxY))
{
// figure out how many units each hashmark must represent
// to respect both pointsPerUnit and minimumPointsPerHashmark
var unitsPerHashmark = minimumPointsPerHashmark / pointsPerUnit
if unitsPerHashmark < 1 {
unitsPerHashmark = pow(10, ceil(log10(unitsPerHashmark)))
} else {
unitsPerHashmark = floor(unitsPerHashmark)
}
let pointsPerHashmark = pointsPerUnit * unitsPerHashmark
// figure out which is the closest set of hashmarks (radiating out from the origin) that are in rect
var startingHashmarkRadius: CGFloat = 1
if !rect.contains(origin) {
let leftx = max(origin.x - rect.maxX, 0)
let rightx = max(rect.minX - origin.x, 0)
let downy = max(origin.y - rect.minY, 0)
let upy = max(rect.maxY - origin.y, 0)
startingHashmarkRadius = min(min(leftx, rightx), min(downy, upy)) / pointsPerHashmark + 1
}
// pick a reasonable number of fraction digits
formatter.maximumFractionDigits = Int(-log10(Double(unitsPerHashmark)))
formatter.minimumIntegerDigits = 1
// now create a bounding box inside whose edges those four hashmarks lie
let bboxSize = pointsPerHashmark * startingHashmarkRadius * 2
var bbox = CGRect(center: origin, size: CGSize(width: bboxSize, height: bboxSize))
// radiate the bbox out until the hashmarks are further out than the rect
while !bbox.contains(rect)
{
let label = formatter.string(from: (origin.x-bbox.minX)/pointsPerUnit)!
if let leftHashmarkPoint = CGPoint(x: bbox.minX, y: origin.y).aligned(inside: rect, usingScaleFactor: contentScaleFactor) {
drawHashmark(at: leftHashmarkPoint, label: .top("-\(label)"))
}
if let rightHashmarkPoint = CGPoint(x: bbox.maxX, y: origin.y).aligned(inside: rect, usingScaleFactor: contentScaleFactor) {
drawHashmark(at: rightHashmarkPoint, label: .top(label))
}
if let topHashmarkPoint = CGPoint(x: origin.x, y: bbox.minY).aligned(inside: rect, usingScaleFactor: contentScaleFactor) {
drawHashmark(at: topHashmarkPoint, label: .left(label))
}
if let bottomHashmarkPoint = CGPoint(x: origin.x, y: bbox.maxY).aligned(inside: rect, usingScaleFactor: contentScaleFactor) {
drawHashmark(at: bottomHashmarkPoint, label: .left("-\(label)"))
}
bbox = bbox.insetBy(dx: -pointsPerHashmark, dy: -pointsPerHashmark)
}
}
}
private func drawHashmark(at location: CGPoint, label: AnchoredText)
{
var dx: CGFloat = 0, dy: CGFloat = 0
switch label {
case .left: dx = Constants.hashmarkSize / 2
case .right: dx = Constants.hashmarkSize / 2
case .top: dy = Constants.hashmarkSize / 2
case .bottom: dy = Constants.hashmarkSize / 2
}
let path = UIBezierPath()
path.move(to: CGPoint(x: location.x-dx, y: location.y-dy))
path.addLine(to: CGPoint(x: location.x+dx, y: location.y+dy))
path.stroke()
label.draw(at: location, usingColor: color)
}
private enum AnchoredText
{
case left(String)
case right(String)
case top(String)
case bottom(String)
static let verticalOffset: CGFloat = 3
static let horizontalOffset: CGFloat = 6
func draw(at location: CGPoint, usingColor color: UIColor) {
let attributes = [
NSFontAttributeName : UIFont.preferredFont(forTextStyle: .footnote),
NSForegroundColorAttributeName : color
]
var textRect = CGRect(center: location, size: text.size(attributes: attributes))
switch self {
case .top: textRect.origin.y += textRect.size.height / 2 + AnchoredText.verticalOffset
case .left: textRect.origin.x += textRect.size.width / 2 + AnchoredText.horizontalOffset
case .bottom: textRect.origin.y -= textRect.size.height / 2 + AnchoredText.verticalOffset
case .right: textRect.origin.x -= textRect.size.width / 2 + AnchoredText.horizontalOffset
}
text.draw(in: textRect, withAttributes: attributes)
}
var text: String {
switch self {
case .left(let text): return text
case .right(let text): return text
case .top(let text): return text
case .bottom(let text): return text
}
}
}
}
private extension CGPoint
{
func aligned(inside bounds: CGRect? = nil, usingScaleFactor scaleFactor: CGFloat = 1.0) -> CGPoint?
{
func align(_ coordinate: CGFloat) -> CGFloat {
return round(coordinate * scaleFactor) / scaleFactor
}
let point = CGPoint(x: align(x), y: align(y))
if let permissibleBounds = bounds, !permissibleBounds.contains(point) {
return nil
}
return point
}
}
private extension NumberFormatter
{
func string(from point: CGFloat) -> String? {
return string(from: NSNumber(value: Double(point)))
}
}
private extension CGRect
{
init(center: CGPoint, size: CGSize) {
self.init(x: center.x-size.width/2, y: center.y-size.height/2, width: size.width, height: size.height)
}
}