Image Slider with UIScrollView - swift3

I am making an image slider that slides automatically every 2 seconds. The code I have wrote moves immediately to the next image without the slide animation, I want to fix it somehow by showing the scrollview scrolling, this is my code:
var imagesArray = [UIImage]()
static var i = 0
override func viewDidLoad() {
super.viewDidLoad()
imagesArray = [image1, image2, image3, image4]
for i in 0..<imagesArray.count{
let imageview = UIImageView()
imageview.image = imagesArray[i]
imageview.contentMode = .scaleAspectFit
let xPosition = self.view.frame.width * CGFloat(i)
imageview.frame = CGRect(x: xPosition, y: 0, width: self.imagesScrollView.frame.width, height: self.imagesScrollView.frame.height)
imagesScrollView.contentSize.width = imagesScrollView.frame.width * CGFloat(i+1)
imagesScrollView.addSubview(imageview)
}
}
override func viewDidAppear(_ animated: Bool) {
let scrollingTimer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(MenuViewController.newStartScrolling), userInfo: nil, repeats: true)
scrollingTimer.fire()
func newStartScrolling()
{
if MenuViewController.i == imagesArray.count {
MenuViewController.i = 0
}
let x = CGFloat(MenuViewController.i) * imagesScrollView.frame.size.width
imagesScrollView.contentOffset = CGPoint(x: x, y: 0)
MenuViewController.i += 1
}
Thank you.

Please try this code.
override func viewDidAppear(_ animated: Bool) {
imagesArray = [UIImage(named: "apple.jpg")!, UIImage(named: "empire.jpg")!, UIImage(named: "ottawa.jpg")!]
imagesScrollView.contentSize = CGSize(width: imagesScrollView.frame.width * CGFloat(imagesArray.count), height: imagesScrollView.frame.height)
for i in 0..<imagesArray.count{
let imageview = UIImageView()
imageview.image = imagesArray[i]
imageview.contentMode = .scaleAspectFill
imageview.clipsToBounds = true
let xPosition = self.imagesScrollView.frame.width * CGFloat(i)
imageview.frame = CGRect(x: xPosition, y: 0, width: self.imagesScrollView.frame.width, height: self.imagesScrollView.frame.height)
print(imageview)
imagesScrollView.addSubview(imageview)
}
let scrollingTimer = Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(ViewController.newStartScrolling), userInfo: nil, repeats: true)
scrollingTimer.fire()
}
func newStartScrolling()
{
if ViewController.i == imagesArray.count {
ViewController.i = 0
}
let x = CGFloat(ViewController.i) * imagesScrollView.frame.size.width
imagesScrollView.setContentOffset(CGPoint(x: x, y: 0), animated: true)
ViewController.i += 1
}
you have to use func setContentOffset(_ contentOffset: CGPoint, animated: Bool) method to change content offset using animation.

#IBOutlet weak var imageView:UIImageView!
var i=Int()
override func viewDidLoad() {
super.viewDidLoad()
Timer.scheduledTimer(timeInterval: 3.0, target: self, selector: #selector(imageChange), userInfo: nil, repeats: true)
// Do any additional setup after loading the view.
}
#objc func imageChange(){
self.imageView.image=images[i]
if i<images.count-1{
i+=1
}
else{
i=0
}
}

Related

Building a page that shows a photo like photo app (zoom and pan)

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")
}
}
}

How to move scrollview with buttons only in SwiftUI

Previously I did with Swift4 UIScrollView which scrolled with buttons and x offset.
In Swift4 I have:
Set Scrolling Enabled and Paging Enabled to false.
Created the margins, offsets for each frame in UIScrollView and changed the position with buttons Back and Next.
Here is the code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var buttonSound: UIButton!
#IBOutlet weak var buttonPrev: UIButton!
#IBOutlet weak var buttonNext: UIButton!
#IBOutlet weak var scrollView: UIScrollView!
var levels = ["level1", "level2", "level3", "level4"]
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var currentLevel = 1
var previousLevel: Int? = nil
override func viewDidLoad() {
super.viewDidLoad()
//Defining the Various Swipe directions (left, right, up, down)
let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(self.handleGesture(gesture:)))
swipeLeft.direction = .left
self.view.addGestureRecognizer(swipeLeft)
let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(self.handleGesture(gesture:)))
swipeRight.direction = .right
self.view.addGestureRecognizer(swipeRight)
addHorizontalLevelsList()
customizeButtons()
resizeSelected()
}
func addHorizontalLevelsList() {
var frame : CGRect?
for i in 0..<levels.count {
let button = UIButton(type: .custom)
let buttonW = screenWidth/3
let buttonH = screenHeight/2
frame = CGRect(x: CGFloat(i+1) * (screenWidth/2) - (buttonW/2),
y: buttonH - 100,
width: buttonW,
height: buttonH)
button.frame = frame!
button.tag = i+1
button.backgroundColor = .lightGray
button.addTarget(self, action: #selector(selectTeam), for: .touchUpInside)
button.setTitle(levels[i], for: .normal)
scrollView.addSubview(button)
}
scrollView.contentSize = CGSize(width: (screenWidth/2 * CGFloat(levels.count)),
height: screenHeight)
scrollView.backgroundColor = .clear
self.view.addSubview(scrollView)
}
func customizeButtons(){
buttonPrev.frame = CGRect(x: 0,
y: (screenHeight/2) - 40,
width: 80, height: 80)
buttonNext.frame = CGRect(x: screenWidth - 80,
y: (screenHeight/2) - 40,
width: 80, height: 80)
buttonPrev.superview?.bringSubviewToFront(buttonPrev)
buttonNext.superview?.bringSubviewToFront(buttonNext)
}
#objc func selectTeam(button: UIButton) {
button.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
UIView.animate(withDuration: 1.0,
delay: 0,
usingSpringWithDamping: CGFloat(0.20),
initialSpringVelocity: CGFloat(6.0),
options: UIView.AnimationOptions.allowUserInteraction,
animations: {
button.transform = CGAffineTransform.identity
},
completion: { Void in() }
)
print(levels[button.tag])
let vc = PopTypeVC(nibName: "PopTypeVC", bundle: nil)
vc.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
self.present(vc, animated: true)
}
#IBAction func prevLevel(_ sender: Any) {
if currentLevel > 0 {
currentLevel -= 1
scroll()
}
}
#IBAction func nextLevel(_ sender: Any) {
if currentLevel < levels.count {
currentLevel += 1
scroll()
}
}
func scroll(){
print(currentLevel)
print(previousLevel as Any)
scrollView.setContentOffset(CGPoint(x: currentLevel * Int(screenWidth/2), y: 0), animated: true)
resizeSelected()
}
// The #objc before func is a must, since we are using #selector (above)
#objc func handleGesture(gesture: UISwipeGestureRecognizer) -> Void {
if gesture.direction == UISwipeGestureRecognizer.Direction.right {
prevLevel(self)
}
else if gesture.direction == UISwipeGestureRecognizer.Direction.left {
nextLevel(self)
}
}
func resizeSelected(){
if previousLevel != nil {
let previousFrame = CGRect(x:CGFloat(previousLevel!) * (screenWidth/2) - (screenWidth/3)/2,
y: (screenHeight/2) - 100,
width: screenWidth/3,
height: screenHeight/2)
scrollView.viewWithTag(previousLevel!)?.frame = previousFrame
}
let currentFrame = CGRect(x: CGFloat(currentLevel) * (screenWidth/2) - (screenWidth/3)/2 - 10,
y: (screenHeight/2) - 110,
width: screenWidth/3 + 20,
height: screenHeight/2 + 20)
scrollView.viewWithTag(currentLevel)?.frame = currentFrame
previousLevel = currentLevel
}
}
The problem is I can't do this with SwiftUI:
struct ContentView: View {
static var levels = ["level1",
"level2",
"level3",
"level4"]
var currentLevel = 1
var previousLevel: Int? = nil
let screenW = UIScreen.main.bounds.width
let screenH = UIScreen.main.bounds.height
let margin1 = 50
let margin2 = 100
let margin3 = 20
let sceneButtonW = 100
let buttonPadding = 40
var body: some View {
ZStack {
// Horizontal list
VStack {
Spacer()
.frame(height: margin2)
ScrollView(.horizontal, showsIndicators: false) {
HStack{
Spacer()
.frame(width: buttonPadding + sceneButtonW/2)
ForEach(0..<ContentView.levels.count) { i in
cardView(i: i).tag(i+1)
}
Spacer()
.frame(width: buttonPadding + sceneButtonW/2)
}
}
Spacer()
.frame(height: margin3)
}
}
.background(Image("bg")
.resizable()
.edgesIgnoringSafeArea(.all)
.aspectRatio(contentMode: .fill))
}
}
Question: Are there any methods to disable automatic scrolling at all and use offsets at ScrollView with SwiftUI?
This already built solution for SwiftUI
https://github.com/fermoya/SwiftUIPager
However, there is no real example.

Can't get UILabel to word wrap

I have used Interface Builder to get UILabelViews to word wrap, but this is my first attempt to do it programmatically. I believe the issue is that although I'm setting:
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
I'm setting a line height which might conflict with these properties?
override func layoutSubviews() {
super.layoutSubviews()
imageViewContent.frame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)
imageViewContent.loadImageWithURL(imageName!)
label.frame = CGRect(x: 0, y: 0, width: frame.size.width-10, height: 21)
label.center = imageViewContent.center
label.textAlignment = .center
label.clipsToBounds = true
label.layer.cornerRadius = 10.0
label.textColor = .white
label.font = UIFont(name: "AvenirNext-DemiBold", size: 15)
label.backgroundColor = UIColor.black.withAlphaComponent(0.5)
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
label.text = photoName
}
As I suspected, hardcoding the height of the label prevented it from word wrapping. I've created a labelHeight variable and added a function at the bottom of the class to calculate the labelHeight based on the content, font size, and labelWidth I set. Code works fine now:
class NTWaterfallViewCell :UICollectionViewCell, NTTansitionWaterfallGridViewProtocol{
var photoName = ""
var imageName : String?
var labelHeight: CGFloat = 0.0
var imageViewContent : UIImageView = UIImageView()
var label = UILabel()
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.lightGray
contentView.addSubview(imageViewContent)
contentView.addSubview(label)
}
override func layoutSubviews() {
super.layoutSubviews()
imageViewContent.frame = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)
imageViewContent.loadImageWithURL(imageName!)
label.text = photoName
labelHeight = heightForText(photoName, width: frame.size.width-10)
label.frame = CGRect(x: 0, y: 0, width: frame.size.width-10, height: labelHeight)
label.center = imageViewContent.center
label.textAlignment = .center
label.clipsToBounds = true
label.layer.cornerRadius = 10.0
label.textColor = .white
label.font = UIFont(name: "AvenirNext-DemiBold", size: 15)
label.backgroundColor = UIColor.black.withAlphaComponent(0.5)
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
}
func heightForText(_ text: String, width: CGFloat) -> CGFloat {
let font = UIFont(name: "AvenirNext-DemiBold", size: 15)
let rect = NSString(string: text).boundingRect(with: CGSize(width: width, height: CGFloat(MAXFLOAT)), options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return ceil(rect.height)
}
}

How Can I Pass the Value of A Textfield to a Button Clicked Function in Swift3?

I need to preform username and login checks upon pressing the login button. I need to do all of this with programmatically. Anyhow, my problem is that when I create a button that connects to a function, the textfields are then out of scope.
import UIKit
class ViewController: UIViewController {
var usernameTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
let usernameTextField: UITextField = UITextField(frame: CGRect(x: 0, y: 0, width: 300.00, height: 30.00));
usernameTextField.center = CGPoint(x: 160, y: 80)
usernameTextField.placeholder = "username"
usernameTextField.text = ""
usernameTextField.borderStyle = UITextBorderStyle.line
usernameTextField.backgroundColor = UIColor.white
usernameTextField.textColor = UIColor.blue
self.view.addSubview(usernameTextField)
let button = UIButton(type: UIButtonType.system) as UIButton
let xPostion:CGFloat = 10
let yPostion:CGFloat = 200
let buttonWidth:CGFloat = 150
let buttonHeight:CGFloat = 45
button.frame = CGRect(x:xPostion, y:yPostion, width:buttonWidth, height:buttonHeight)
button.backgroundColor = UIColor.lightGray
button.setTitle("Submit", for: UIControlState.normal)
button.tintColor = UIColor.black
button.addTarget(self, action: #selector(ViewController.buttonAction(_:)), for: .touchUpInside)
self.view.addSubview(button)
}
func buttonAction(_ sender:UIButton!) {
let username = usernameTextField.text
print("Username value is \(String(describing: username))!")
print("Button tapped")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
How can I get a usernameTextField at buttonAction function ?
usernameTextField is not out of scope, your app will just crash since you have hidden the property of that name with a local variable inside viewDidLoad.
You should change your let-line to usernameTextField = which does not create a local variable of the same name but assigns something to the property instead:
override func viewDidLoad() {
super.viewDidLoad()
usernameTextField = UITextField(frame: CGRect(x: 0, y: 0, width: 300.00, height: 30.00));
usernameTextField.center = CGPoint(x: 160, y: 80)
usernameTextField.placeholder = "username"
usernameTextField.text = ""
usernameTextField.borderStyle = UITextBorderStyle.line
usernameTextField.backgroundColor = UIColor.white
usernameTextField.textColor = UIColor.blue
self.view.addSubview(usernameTextField)
...
}

AxesDrawer cannot draw swift3 xcode 8

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)
}
}