How to find the new center of an UIImageView after scaling? - ios

I am creating an app where the user can add an image to a "canvas" and resize and move the image around using pinch and pan gesture recognizers. The image view is a custom one I created using this article:
Bezier Paths and Gesture Recognizers
This works really nicely. The image resizes and moves very smoothly. I can capture the center and the size of the image after the pan and pinch gestures. The problem is that after I save the size and coordinates of the image it isn't respecting those when I load it back into the "canvas". It is as if the center is offset by "X" number of pixels.
Here is my code for making the resizable and movable image view:
‘’’
import UIKit
import Foundation
class MovableImage: UIImageView {
let size: CGFloat = 150.0
var imageMovedHandler:((_ x: CGFloat, _ y: CGFloat) -> ())?
var imageDeletedHandler:((_ delete: Bool) -> ())?
var longPressHandler:((_ selected: Bool) -> ())?
var imageSizeChangedHandler:((_ newImageView: MovableImage) -> ())?
let deleteButton = UIButton(type: .close)
init(origin: CGPoint) {
super.init(frame: CGRect(x: origin.x, y: origin.y, width: size, height: size)) //
debugCenterDot()
initGestureRecognizers()
}
//added a dot to try and understand what is happening with the "center" of the imageview, but it didn't show in the center of the imageview
func debugCenterDot() {
let dot = UIBezierPath(ovalIn: CGRect(x: self.center.x, y: self.center.y, width: 15, height: 15))
let dotLayer = CAShapeLayer()
dotLayer.path = dot.cgPath
dotLayer.strokeColor = UIColor.yellow.cgColor
self.layer.addSublayer(dotLayer)
self.setNeedsDisplay()
}
internal func addButton() {
deleteButton.tintColor = UIColor.red
deleteButton.backgroundColor = UIColor.white
deleteButton.addTarget(self, action: #selector(deleteSelf(sender:)), for: .touchUpInside)
deleteButton.frame = .zero //CGRect(x: 8, y: 8, width: 15, height: 15)
deleteButton.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(deleteButton)
NSLayoutConstraint.activate([
deleteButton.widthAnchor.constraint(equalToConstant: 15),
deleteButton.widthAnchor.constraint(equalTo: deleteButton.heightAnchor),
deleteButton.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 8),
deleteButton.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 8),
])
}
#objc func deleteSelf(sender: UIButton) {
imageDeletedHandler?(true)
self.removeFromSuperview()
}
func initGestureRecognizers() {
let panGR = UIPanGestureRecognizer(target: self, action: #selector(didPan(panGR:)))
addGestureRecognizer(panGR)
let pinchGR = UIPinchGestureRecognizer(target: self, action: #selector(didPinch(pinchGR:)))
addGestureRecognizer(pinchGR)
let longPressGR = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress(longPressGR:)))
longPressGR.minimumPressDuration = 1
addGestureRecognizer(longPressGR)
}
#objc func didLongPress(longPressGR: UILongPressGestureRecognizer) {
self.superview!.bringSubviewToFront(self)
self.layer.borderWidth = 2
self.layer.borderColor = UIColor.red.cgColor
addButton()
longPressHandler?(true)
}
#objc func didPan(panGR: UIPanGestureRecognizer) {
self.superview!.bringSubviewToFront(self)
if self.layer.borderWidth == 2 {
let translation = panGR.translation(in: self)
print("BEFORE PAN: \(self.center)")
self.center.x += translation.x
self.center.y += translation.y
print("AFTER PAN: \(self.center)")
panGR.setTranslation(CGPoint.zero, in: self)
if panGR.state == .ended {
imageMovedHandler?(self.center.x, self.center.y)
self.layer.borderWidth = 0
self.layer.borderColor = nil
self.deleteButton.removeFromSuperview()
}
}
}
#objc func didPinch(pinchGR: UIPinchGestureRecognizer) {
self.superview?.bringSubviewToFront(self)
if self.layer.borderWidth == 2 {
let scale = pinchGR.scale
self.transform = CGAffineTransform(scaleX: scale, y: scale)
if pinchGR.state == .ended {
imageSizeChangedHandler?(self)
}
}
}
func scaleOf(transform: CGAffineTransform) -> CGPoint {
let xscale = sqrt(transform.a * transform.a + transform.c * transform.c)
let yscale = sqrt(transform.b * transform.b + transform.d * transform.d)
return CGPoint(x: xscale, y: yscale)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
‘’’
And here is how I am loading it back into the "canvas" and saving it back to CoreData (save functions in closures near the bottom of the load function):
'''
func loadImages(new: Bool, enableInteraction: Bool) {
let pagePhotos = LoadPagePhotos()
var pageImages: [NSManagedObject] = []
var x: CGFloat?
var y: CGFloat?
var width: CGFloat?
var height: CGFloat?
var image: UIImage?
if isYear {
pageImages = pagePhotos.resetYearPhotosPositions(journalName: journalName, yearPosition: yearPosition)
} else {
pageImages = pagePhotos.resetPhotosPositions(journalName: journalName, monthName: monthName, weekPosition: positionWeek)
}
scrollView.mainView.newImages.forEach { i in
i.removeFromSuperview()
}
scrollView.mainView.newImages.removeAll()
if pageImages.count > 0 {
pageImages.forEach{ (photo) in
x = CGFloat((photo.value(forKey: "pageImageX") as? Float)!)
y = CGFloat((photo.value(forKey: "pageImageY") as? Float)!)
height = CGFloat((photo.value(forKey: "pageImageSizeHeight") as? Float)!)
width = CGFloat((photo.value(forKey: "pageImageSizeWidth") as? Float)!)
image = photo.value(forKey: "image") as? UIImage
let thisImage: MovableImage = MovableImage(origin: CGPoint.zero)
thisImage.contentMode = .scaleAspectFit
thisImage.center = CGPoint(x: x!, y: y!)
thisImage.image = image!
thisImage.frame.size.height = height!
thisImage.frame.size.width = width!
scrollView.mainView.addSubview(thisImage)
scrollView.mainView.newImages.append(thisImage)
if enableInteraction {
thisImage.isUserInteractionEnabled = true
} else {
thisImage.isUserInteractionEnabled = false
}
thisImage.layer.zPosition = 1
thisImage.layer.borderWidth = 0
if new {
imageOptionsMenuView.isHidden = false
} else {
imageOptionsMenuView.isHidden = true
}
movableImage = thisImage
//for clarity sake I moved the save functions to separate block here in stack overflow so it is easier to read
}
}
'''
The closures "imageMovedHandler" and "imageSizeChangedHandler" are used to detect when moving and resizing is done and I save the image to CoreData. Here they are:
'''
if movableImage != nil {
movableImage?.imageMovedHandler = { [unowned self] (x, y) in
if self.isYear {
if self.scrollView.mainView.newImages.count > 0 {
for (idx, i) in self.scrollView.mainView.newImages.enumerated() {
if i.layer.borderWidth == 2 {
let id = idx + 1
_ = LoadPagePhotos().updateYearPhoto(journalName: self.journalName, yearPosition: self.yearPosition, pageImageId: id, imageHeight: Float(i.frame.size.height), imageWidth: Float(i.frame.size.width), imageX: Float(i.center.x), imageY: Float(i.center.y), pagePhoto: i.image!, photoPath: nil)
}
}
}
} else {
if self.scrollView.mainView.newImages.count > 0 {
for (idx, i) in self.scrollView.mainView.newImages.enumerated() {
if i.layer.borderWidth == 2 {
let id = idx + 1
_ = LoadPagePhotos().updatePhoto(journalName: self.journalName, monthName: self.monthName, weekPosition: self.positionWeek, pageImageId: id, imageHeight: Float(i.frame.size.height), imageWidth: Float(i.frame.size.width), imageX: Float(i.center.x), imageY: Float(i.center.y), pagePhoto: i.image!, photoPath: nil)
}
}
}
}
self.loadImages(new: false, enableInteraction: true)
}
movableImage?.imageSizeChangedHandler = { [unowned self] (newImageView) in
var id = 0
var img = 0
if self.isYear {
if self.scrollView.mainView.newImages.count > 0 {
for (idx, i) in self.scrollView.mainView.newImages.enumerated() {
if i.layer.borderWidth == 2 {
id = idx + 1
img = idx
}
}
self.scrollView.mainView.newImages[img] = newImageView
_ = LoadPagePhotos().updateYearPhoto(journalName: self.journalName, yearPosition: self.yearPosition, pageImageId: id, imageHeight: Float(newImageView.frame.size.height), imageWidth: Float(newImageView.frame.size.width), imageX: Float(newImageView.center.x), imageY: Float(newImageView.center.y), pagePhoto: newImageView.image!, photoPath: nil)
}
} else {
if self.scrollView.mainView.newImages.count > 0 {
for (idx, i) in self.scrollView.mainView.newImages.enumerated() {
if i.layer.borderWidth == 2 {
id = idx + 1
img = idx
}
}
self.scrollView.mainView.newImages[img] = newImageView
_ = LoadPagePhotos().updatePhoto(journalName: self.journalName, monthName: self.monthName, weekPosition: self.positionWeek, pageImageId: id, imageHeight: Float(newImageView.frame.size.height), imageWidth: Float(newImageView.frame.size.width), imageX: Float(newImageView.center.x), imageY: Float(newImageView.center.y), pagePhoto: newImageView.image!, photoPath: nil)
}
}
self.loadImages(new: false, enableInteraction: true)
}
}
}
'''
Here is an image of what is happening when I move the image around the canvas:
This first image shows where I stopped moving the image to:
This image shows where the image was loaded after saving the size and coordinates:
The desired outcome is:
When pinching, panning, and saving the image and then loading, the image retains its current coordinates and size in the canvas.
EDIT:
It should also be noted that the offset of the image when moving it only happens after "scaling

Related

Swipeable CIFilter over video

I am currently trying to implement something similar to Instagram's story feature where you take a picture or a video and when swiping left or right you change the current filter over the content. ( here is an example of what I managed to do in my app for images https://imgur.com/a/pYKrPkA )
As you can see in the example, I got it done for images but now my problem is that I am trying to make if work for videos aswell and I am a bit lost from where to start.
final class Filter: NSObject {
var isEnabled: Bool = true
var overlayImage: CIImage?
var ciFilter: CIFilter?
init(ciFilter: CIFilter?) {
self.ciFilter = ciFilter
super.init()
}
/// Empty filter for the original photo
static func emptyFilter() -> Filter {
return Filter(ciFilter: nil)
}
func imageByProcessingImage(_ image: CIImage, at time: CFTimeInterval) -> CIImage? {
guard isEnabled else { return image }
var image = image
if let overlayImage = overlayImage {
image = overlayImage.composited(over: image)
}
guard let ciFilter = ciFilter else {
return image
}
ciFilter.setValue(image, forKey: kCIInputImageKey)
return ciFilter.value(forKey: kCIOutputImageKey) as? CIImage
}
}
class StoriesImageView: UIView {
private var metalView: MTKView?
private var ciImage: CIImage?
private var preferredCIImageTransform: CGAffineTransform?
private let device = MTLCreateSystemDefaultDevice()
private var commandQueue: MTLCommandQueue?
private var context: CIContext?
override func layoutSubviews() {
super.layoutSubviews()
metalView?.frame = bounds
}
override func setNeedsDisplay() {
super.setNeedsDisplay()
metalView?.setNeedsDisplay()
}
func setImage(with image: UIImage) {
preferredCIImageTransform = preferredCIImageTransform(from: image)
if let cgImage = image.cgImage {
ciImage = CIImage(cgImage: cgImage)
loadContextIfNeeded()
}
setNeedsDisplay()
}
/// Return the image fitted to 1080x1920.
func renderedUIImage() -> UIImage? {
return renderedUIImage(in: CGRect(origin: .zero, size: CGSize(width: 1080, height: 1920)))
}
/// Returns CIImage in fitted to main screen bounds.
func renderedCIIImage() -> CIImage? {
return renderedCIImage(in: CGRect(rect: bounds, contentScale: UIScreen.main.scale))
}
func renderedUIImage(in rect: CGRect) -> UIImage? {
if let image = renderedCIImage(in: rect), let context = context {
if let imageRef = context.createCGImage(image, from: image.extent) {
return UIImage(cgImage: imageRef)
}
}
return nil
}
func renderedCIImage(in rect: CGRect) -> CIImage? {
if var image = ciImage, let transform = preferredCIImageTransform {
image = image.transformed(by: transform)
return scaleAndResize(image, for: rect)
}
return nil
}
private func cleanupContext() {
metalView?.removeFromSuperview()
metalView?.releaseDrawables()
metalView = nil
}
private func loadContextIfNeeded() {
setContext()
}
private func setContext() {
let mView = MTKView(frame: bounds, device: device)
mView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
mView.framebufferOnly = false
mView.enableSetNeedsDisplay = true
mView.contentScaleFactor = contentScaleFactor
mView.delegate = self
metalView = mView
commandQueue = device?.makeCommandQueue()
context = CIContext(mtlDevice: device!)
insertSubview(metalView!, at: 0)
}
private func scaleAndResize(_ image: CIImage, for rect: CGRect) -> CIImage {
let imageSize = image.extent.size
let horizontalScale = rect.size.width / imageSize.width
let verticalScale = rect.size.height / imageSize.height
let scale = min(horizontalScale, verticalScale)
return image.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
}
private func preferredCIImageTransform(from image: UIImage) -> CGAffineTransform {
if image.imageOrientation == .up {
return .identity
}
var transform: CGAffineTransform = .identity
switch image.imageOrientation {
case .down, .downMirrored:
transform = transform.translatedBy(x: image.size.width, y: image.size.height)
transform = transform.rotated(by: .pi)
case .left, .leftMirrored:
transform = transform.translatedBy(x: image.size.width, y: 0)
transform = transform.rotated(by: .pi / 2)
case .right, .rightMirrored:
transform = transform.translatedBy(x: 0, y: image.size.height)
transform = transform.rotated(by: .pi / -2)
case .up, .upMirrored: break
#unknown default: fatalError("Unknown image orientation")
}
switch image.imageOrientation {
case .upMirrored, .downMirrored:
transform = transform.translatedBy(x: image.size.width, y: 0)
transform = transform.scaledBy(x: -1, y: 1)
case .leftMirrored, .rightMirrored:
transform = transform.translatedBy(x: image.size.height, y: 0)
transform = transform.scaledBy(x: -1, y: 1)
case .up, .down, .left, .right: break
#unknown default: fatalError("Unknown image orientation")
}
return transform
}
}
extension StoriesImageView: MTKViewDelegate {
func draw(in view: MTKView) {
autoreleasepool {
let rect = CGRect(rect: view.bounds, contentScale: UIScreen.main.scale)
if let image = renderedCIImage(in: rect) {
let commandBuffer = commandQueue?.makeCommandBuffer()
guard let drawable = view.currentDrawable else {
return
}
let heightDifference = (view.drawableSize.height - image.extent.size.height) / 2
let destination = CIRenderDestination(width: Int(view.drawableSize.width),
height: Int(view.drawableSize.height - heightDifference),
pixelFormat: view.colorPixelFormat,
commandBuffer: commandBuffer,
mtlTextureProvider: { () -> MTLTexture in
return drawable.texture
})
_ = try? context?.startTask(toRender: image, to: destination)
commandBuffer?.present(drawable)
commandBuffer?.commit()
}
}
}
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
}
final class StoriesSwipeableImageView: StoriesImageView {
private let scrollView: UIScrollView = UIScrollView()
private let preprocessingFilter: Filter? = nil
var isRefreshingAutomaticallyWhenScrolling: Bool = true
var filters: [Filter]? {
didSet {
updateScrollViewContentSize()
updateCurrentSelected(notify: true)
}
}
var selectedFilter: Filter? {
didSet {
if selectedFilter != oldValue {
setNeedsLayout()
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func layoutSubviews() {
super.layoutSubviews()
scrollView.frame = bounds
updateScrollViewContentSize()
}
private func setup() {
scrollView.delegate = self
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
scrollView.bounces = true
scrollView.alwaysBounceVertical = true
scrollView.alwaysBounceHorizontal = true
scrollView.backgroundColor = .clear
addSubview(scrollView)
}
private func updateScrollViewContentSize() {
let filterCount = filters?.count ?? 0
scrollView.contentSize = CGSize(width: filterCount * Int(frame.size.width) * 3,
height: Int(frame.size.height))
if let selectedFilter = selectedFilter {
scroll(to: selectedFilter, animated: false)
}
}
private func scroll(to filter: Filter, animated: Bool) {
if let index = filters?.firstIndex(where: { $0 === filter }) {
let contentOffset = CGPoint(x: scrollView.contentSize.width / 3 + scrollView.frame.size.width * CGFloat(index), y: 0)
scrollView.setContentOffset(contentOffset, animated: animated)
updateCurrentSelected(notify: false)
} else {
fatalError("Filter is not available in filters collection")
}
}
private func updateCurrentSelected(notify: Bool) {
guard frame.size.width != 0 else { return }
let filterCount = filters?.count ?? 0
let selectedIndex = Int(scrollView.contentOffset.x + scrollView.frame.size.width / 2) / Int(scrollView.frame.size.width) % filterCount
var newFilterGroup: Filter?
if selectedIndex >= 0 && selectedIndex < filterCount {
newFilterGroup = filters?[selectedIndex]
} else {
fatalError("Invalid contentOffset")
}
if selectedFilter != newFilterGroup {
selectedFilter = newFilterGroup
if notify {
// Notify delegate?
}
}
}
override func renderedCIImage(in rect: CGRect) -> CIImage? {
guard var image = super.renderedCIImage(in: rect) else {
print("Failed to render image")
return nil
}
let timeinterval: CFTimeInterval = 0
if let preprocessingFilter = self.preprocessingFilter {
image = preprocessingFilter.imageByProcessingImage(image, at: timeinterval)!
}
let extent = image.extent
let contentSize = scrollView.bounds.size
if contentSize.width == 0 {
return image
}
let filtersCount = filters?.count ?? 0
if filtersCount == 0 {
return image
}
let ratio = scrollView.contentOffset.x / contentSize.width
var index = Int(ratio)
let upIndex = Int(ceil(ratio))
let remaningRatio = ratio - CGFloat(index)
var xImage = extent.size.width * -remaningRatio
var outputImage: CIImage? = CIImage(color: CIColor(red: 0, green: 0, blue: 0))
while index <= upIndex {
let currentIndex = index % filtersCount
let filter = filters?[currentIndex]
var filteredImage = filter?.imageByProcessingImage(image, at: timeinterval)
filteredImage = filteredImage?.cropped(to:
CGRect(x: extent.origin.x + xImage,
y: extent.origin.y,
width: extent.size.width,
height: extent.size.height)
)
guard let output = outputImage else { return nil }
outputImage = filteredImage?.composited(over: output)
xImage += extent.size.width
index += 1
}
outputImage = outputImage?.cropped(to: extent)
return outputImage
}
}
extension StoriesSwipeableImageView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let width = scrollView.frame.size.width
let contentOffsetX = scrollView.contentOffset.x
let contentSizeWidth = scrollView.contentSize.width
let normalWidth = CGFloat(filters?.count ?? 0) * width
if width > 0 && contentSizeWidth > 0 {
if contentOffsetX <= 0 {
scrollView.contentOffset = CGPoint(x: contentOffsetX + normalWidth, y: scrollView.contentOffset.y)
} else if contentOffsetX + width >= contentSizeWidth {
scrollView.contentOffset = CGPoint(x: contentOffsetX - normalWidth, y: scrollView.contentOffset.y)
}
}
if isRefreshingAutomaticallyWhenScrolling {
setNeedsDisplay()
}
}
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
updateCurrentSelected(notify: true)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
updateCurrentSelected(notify: true)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
updateCurrentSelected(notify: true)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
updateCurrentSelected(notify: true)
}
}
}
These 3 are the classes that do the magic for the image part. Does anyone have a suggestion or a starting point for this? I tried looking over at https://github.com/rFlex/SCRecorder but I get a bit lost in Obj-C.
In iOS 9 / OS X 10.11 / tvOS, there's a convenience method for applying CIFilters to video. It works on an AVVideoComposition, so you can use it both for playback and for file-to-file import/export. See AVVideoComposition.init(asset:applyingCIFiltersWithHandler:) for the method docs.
There's an example in Apple's Core Image Programming Guide, too:
let filter = CIFilter(name: "CIGaussianBlur")!
let composition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in
// Clamp to avoid blurring transparent pixels at the image edges
let source = request.sourceImage.clampingToExtent()
filter.setValue(source, forKey: kCIInputImageKey)
// Vary filter parameters based on video timing
let seconds = CMTimeGetSeconds(request.compositionTime)
filter.setValue(seconds * 10.0, forKey: kCIInputRadiusKey)
// Crop the blurred output to the bounds of the original image
let output = filter.outputImage!.cropping(to: request.sourceImage.extent)
// Provide the filter output to the composition
request.finish(with: output, context: nil)
})
That part sets up the composition. After you've done that, you can either play it by assigning it to an AVPlayer or write it to a file with AVAssetExportSession. Since you're after the latter, here's an example of that:
let export = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset1920x1200)
export.outputFileType = AVFileTypeQuickTimeMovie
export.outputURL = outURL
export.videoComposition = composition
export.exportAsynchronouslyWithCompletionHandler(/*...*/)
There's a bit more about this in the WWDC15 session on Core Image, starting around 20 minutes in.

My UIView doesn't show up as I try to set constraints relative to safe area programmatically

I have a StackContainerView inside my main view controller called TodayPicksViewController. I am trying to programmatically set the StackContainerView to fill up the whole view controller side to side, with around 50 from top and bottom (just like a Tinder card).
However, as I try to implement constraints relative to safe area as follows(as other answers on StackOverflow suggest), turned out the StackContainerView doesn't show up at all. I don't know where the problem is.
Please advice.
Code of my main view controller, TodayPicksViewController:
class TodayPicksViewController: UIViewController {
//MARK: - Properties
var viewModelData = [CardsDataModel(bgColor: UIColor(red:0.96, green:0.81, blue:0.46, alpha:1.0), text: "Hamburger", image: "hamburger"),
CardsDataModel(bgColor: UIColor(red:0.29, green:0.64, blue:0.96, alpha:1.0), text: "Puppy", image: "puppy"),
CardsDataModel(bgColor: UIColor(red:0.29, green:0.63, blue:0.49, alpha:1.0), text: "Poop", image: "poop"),
CardsDataModel(bgColor: UIColor(red:0.69, green:0.52, blue:0.38, alpha:1.0), text: "Panda", image: "panda"),
CardsDataModel(bgColor: UIColor(red:0.90, green:0.99, blue:0.97, alpha:1.0), text: "Subway", image: "subway"),
CardsDataModel(bgColor: UIColor(red:0.83, green:0.82, blue:0.69, alpha:1.0), text: "Robot", image: "robot")]
var stackContainer : StackContainerView!
private let spinner = JGProgressHUD(style: .dark)
private var users = [[String: String]]()
private var results = [SearchResult]()
private var hasFetched = false
var divisor: CGFloat!
private let noResultsLabel: UILabel = {
let label = UILabel()
label.isHidden = true
label.text = "No Results"
label.textAlignment = .center
label.textColor = .green
label.font = .systemFont(ofSize: 21, weight: .medium)
return label
}()
override func loadView() {
view = UIView()
stackContainer = StackContainerView()
view.addSubview(stackContainer)
stackContainer.translatesAutoresizingMaskIntoConstraints = false
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(noResultsLabel)
configureStackContainer()
stackContainer.dataSource = self
}
#IBAction func panMatch(_ sender: UIPanGestureRecognizer) {
let match = sender.view!
let point = sender.translation(in: view)
let xFromCenter = match.center.x - view.center.x
print(xFromCenter)
match.center = CGPoint(x: view.center.x + point.x, y: view.center.y + point.y)
match.transform = CGAffineTransform(rotationAngle: xFromCenter/divisor)
if sender.state == UIGestureRecognizer.State.ended {
if match.center.x < 75 {
// Move off to the left side
UIView.animate(withDuration: 0.3, animations: {
match.center = CGPoint(x: match.center.x - 200, y: match.center.y + 75)
match.alpha = 0
})
return
} else if match.center.x > (view.frame.width - 75) {
// Move off to the right side
UIView.animate(withDuration: 0.3, animations: {
match.center = CGPoint(x: match.center.x + 200, y: match.center.y + 75)
match.alpha = 0
})
return
}
// resetCard()
}
}
private var loginObserver: NSObjectProtocol?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
validateAuth()
}
private func validateAuth() {
if FirebaseAuth.Auth.auth().currentUser == nil {
let vc = SignInViewController()
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: false)
}
}
#objc private func pageControlDidChange(_ sender: UIPageControl) {
let current = sender.currentPage
// scrollView.setContentOffset(CGPoint(x: CGFloat(current) * view.frame.size.width,
// y: 70), animated: true)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
//MARK: - Configurations
func configureStackContainer() {
stackContainer.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
stackContainer.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: -60).isActive = true
// stackContainer.widthAnchor.constraint(equalToConstant: 300).isActive = true
// stackContainer.heightAnchor.constraint(equalToConstant: 400).isActive = true
stackContainer.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
stackContainer.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
stackContainer.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor).isActive = true
stackContainer.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
func updateUI() {
if results.isEmpty {
noResultsLabel.isHidden = false
}
else {
noResultsLabel.isHidden = true
}
}
func calcAge(birthday: Date) -> Int {
let dateFormater = DateFormatter()
dateFormater.dateFormat = "MM/dd/yyyy"
// let birthdayDate = dateFormater.date(from: birthday)
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let calcAge = calendar.components(.year, from: birthday, to: now, options: [])
let age = calcAge.year
return age!
}
extension TodayPicksViewController : SwipeCardsDataSource {
func numberOfCardsToShow() -> Int {
return viewModelData.count
}
func card(at index: Int) -> SwipeCardView {
let card = SwipeCardView()
card.dataSource = viewModelData[index]
return card
}
func emptyView() -> UIView? {
return nil
}
}
Probably doesn't matter, but here is my code for the StackContainerView:
class StackContainerView: UIView, SwipeCardsDelegate {
//MARK: - Properties
var numberOfCardsToShow: Int = 0
var cardsToBeVisible: Int = 3
var cardViews : [SwipeCardView] = []
var remainingcards: Int = 0
let horizontalInset: CGFloat = 10.0
let verticalInset: CGFloat = 10.0
var visibleCards: [SwipeCardView] {
return subviews as? [SwipeCardView] ?? []
}
var dataSource: SwipeCardsDataSource? {
didSet {
reloadData()
}
}
//MARK: - Init
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reloadData() {
removeAllCardViews()
guard let datasource = dataSource else { return }
setNeedsLayout()
layoutIfNeeded()
numberOfCardsToShow = datasource.numberOfCardsToShow()
remainingcards = numberOfCardsToShow
for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) {
addCardView(cardView: datasource.card(at: i), atIndex: i )
}
}
//MARK: - Configurations
private func addCardView(cardView: SwipeCardView, atIndex index: Int) {
cardView.delegate = self
addCardFrame(index: index, cardView: cardView)
cardViews.append(cardView)
insertSubview(cardView, at: 0)
remainingcards -= 1
}
func addCardFrame(index: Int, cardView: SwipeCardView) {
var cardViewFrame = bounds
let horizontalInset = (CGFloat(index) * self.horizontalInset)
let verticalInset = CGFloat(index) * self.verticalInset
cardViewFrame.size.width -= 2 * horizontalInset
cardViewFrame.origin.x += horizontalInset
cardViewFrame.origin.y += verticalInset
cardView.frame = cardViewFrame
}
private func removeAllCardViews() {
for cardView in visibleCards {
cardView.removeFromSuperview()
}
cardViews = []
}
func swipeDidEnd(on view: SwipeCardView) {
guard let datasource = dataSource else { return }
view.removeFromSuperview()
if remainingcards > 0 {
let newIndex = datasource.numberOfCardsToShow() - remainingcards
addCardView(cardView: datasource.card(at: newIndex), atIndex: 2)
for (cardIndex, cardView) in visibleCards.reversed().enumerated() {
UIView.animate(withDuration: 0.2, animations: {
cardView.center = self.center
self.addCardFrame(index: cardIndex, cardView: cardView)
self.layoutIfNeeded()
})
}
}else {
for (cardIndex, cardView) in visibleCards.reversed().enumerated() {
UIView.animate(withDuration: 0.2, animations: {
cardView.center = self.center
self.addCardFrame(index: cardIndex, cardView: cardView)
self.layoutIfNeeded()
})
}
}
}
}
According to the apple developer doc for loadView(), they said "The view controller calls this method when its view property is requested but is currently nil. This method loads or creates a view and assigns it to the view property." This might be the cause of the problem. I would recommend you to perform the view set up operations in viewDidLoad or other proper lifecycle methods. Based on my understanding, this line view = UIView() isn't necessary. In your configureStackContainer() func, you set the centerX and centerY anchor and then set the top, leading, trailing, bottom anchor again. This may also raise the constraint conflicts. I think you don't need to specify centerX and centerY anchor if you want to constraint with top, leading, trailing and bottom and vice versa. I hope this will be helpful.

How to create swipe to start button with moving arrows

I want to create the exactly the same swipe button like this https://github.com/shadowfaxtech/proSwipeButton .
I was wondering how to change the arrow of the button on user touches
I was doing this for getting swipe action.
let rightSwipe = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipes(_:)))
rightSwipe.direction = .right
view.addGestureRecognizer(rightSwipe)
but the thing is how to add arrows to button which change there position on user touches.
Here is the code I have written for swiping over the button. You assign image to the image view.
func createSwipeButton() {
let button = UIButton.init(type: .custom)
button.backgroundColor = UIColor.brown
button.setTitle("PLACE ORDER", for: .normal)
button.frame = CGRect.init(x: 10, y: 200, width: self.view.frame.size.width-20, height: 100)
button.addTarget(self, action: #selector(swiped(_:event:)), for: .touchDragInside)
button.addTarget(self, action: #selector(swipeEnded(_:event:)), for: .touchUpInside)
self.view.addSubview(button)
let swipableView = UIImageView.init()
swipableView.frame = CGRect.init(x: 0, y: 0, width: 20, height: button.frame.size.height)
swipableView.tag = 20
swipableView.backgroundColor = UIColor.white
button.addSubview(swipableView)
}
#objc func swiped(_ sender : UIButton, event: UIEvent) {
let swipableView = sender.viewWithTag(20)!
let centerPosition = location(event: event, subView: swipableView, superView: sender,isSwiping: true)
UIView.animate(withDuration: 0.2) {
swipableView.center = centerPosition
}
}
#objc func swipeEnded(_ sender : UIButton, event: UIEvent) {
let swipableView = sender.viewWithTag(20)!
let centerPosition = location(event: event, subView: swipableView, superView: sender, isSwiping: false)
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 5, options: .curveEaseInOut, animations: {
swipableView.center = centerPosition
}) { _ in}
}
func location(event: UIEvent, subView: UIView, superView: UIButton, isSwiping: Bool) -> CGPoint {
if let touch = event.touches(for: superView)?.first{
let previousLocation = touch.previousLocation(in: superView)
let location = touch.location(in: superView)
let delta_x = location.x - previousLocation.x;
print(subView.center.x + delta_x)
var centerPosition = CGPoint.init(x: subView.center.x + delta_x, y: subView.center.y)
let minX = subView.frame.size.width/2
let maxX = superView.frame.size.width - subView.frame.size.width/2
centerPosition.x = centerPosition.x < minX ? minX : centerPosition.x
centerPosition.x = centerPosition.x > maxX ? maxX : centerPosition.x
if !isSwiping{
let normalPosition = superView.frame.size.width * 0.5
centerPosition.x = centerPosition.x > normalPosition ? maxX : minX
centerPosition.x = centerPosition.x <= normalPosition ? minX : centerPosition.x
}
return centerPosition
}
return CGPoint.zero
}
Complete project is on github: https://github.com/IamSaurav/SwipeButton
Mmm what about something like this?
You can add an UIImage in the storyboard in the swipeImage var.
The best effect is done if the image has the same color of the text.
import UIKit
#IBDesignable
class UISwipeableLabel: UILabel {
#IBInspectable var swipeImage: UIImage? {
didSet {
configureSwipeImage()
}
}
private var swipeImageView: UIImageView?
private var rightSwipe: UIPanGestureRecognizer?
private var shouldActivateButton = true
override func awakeFromNib() {
super.awakeFromNib()
configureSwipeImage()
clipsToBounds = true
}
}
private extension UISwipeableLabel {
#objc func handleSwipes(_ sender:UIPanGestureRecognizer) {
if let centerX = swipeImageView?.center.x {
let translation = sender.translation(in: self)
let percent = centerX/frame.width
if sender.state == .changed {
if centerX < frame.width - frame.height/2 {
swipeImageView?.center.x = centerX + translation.x
sender.setTranslation(CGPoint.zero, in: swipeImageView)
} else {
swipeImageView?.center.x = frame.width - frame.height/2
if shouldActivateButton {
activateButton()
}
}
}
if sender.state == .ended || sender.state == .cancelled || sender.state == .failed {
if shouldActivateButton {
UIView.animate(withDuration: 0.25 * TimeInterval(percent)) {
self.swipeImageView?.center.x = self.frame.height/2
}
}
}
}
}
func configureSwipeImage() {
if swipeImageView != nil {
swipeImageView?.removeFromSuperview()
}
swipeImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: frame.height, height: frame.height))
if let swipeImageView = swipeImageView {
swipeImageView.image = swipeImage
swipeImageView.isUserInteractionEnabled = true
swipeImageView.alpha = 0.5
addSubview(swipeImageView)
rightSwipe = UIPanGestureRecognizer(target: self, action: #selector(handleSwipes(_:)))
if let rightSwipe = rightSwipe {
swipeImageView.addGestureRecognizer(rightSwipe)
}
}
}
func activateButton() {
print("*** DO YOUR STUFF HERE ***")
}
}
You start with a UILabel and if you want, change it to use autolayout.

Drop annotation on virtual map swift3

please help me out.
First time working on a project, with iBeacon involved, which creating Virtual map of inside of a store.
I know how to drop pins, with a MapKit, but how can i do it, if i only have CGPoints on View ?
I managed how to drip UIImage(with pin image), on a view. But when i rotate or pinch, it's not stay on that coordinates that i dropped.
Here's a code :
import UIKit
class MapViewController: UIViewController{
private var scaleView: CGFloat = 1
private var rotateView: CGFloat = 0
private var anchorPoint = CGPoint(x: 0.5, y: 0.5)
private let gestureRecognizerDelegate = GestureRecognizerDelegate()
#IBOutlet weak var mapView: MapView!
#IBOutlet var pinchGestureRecognizer: UIPinchGestureRecognizer!
#IBOutlet var panGestureRecognizer: UIPanGestureRecognizer!
#IBOutlet var rotateGestureRecognizer: UIRotationGestureRecognizer!
#IBOutlet weak var pin: UIImageView!
override func viewDidAppear(_ animated: Bool) {
if !cartIsEmpty {
cartBtn.setImage(UIImage(named: "haveitem"), for: .normal)
} else {
cartBtn.setImage(UIImage(named: "2772"), for: .normal)
}
cartBtn.addTarget(self, action: #selector(openCart), for: .touchUpInside)
let item1 = UIBarButtonItem(customView: cartBtn)
self.navigationItem.rightBarButtonItem = item1
}
override func viewDidDisappear(_ animated: Bool) {
cartBtn.removeTarget(self, action: #selector(openCart), for: .touchUpInside)
}
override func viewDidLoad() {
super.viewDidLoad()
ApplicationManager.sharedInstance.onApplicationStart()
NotificationCenter.default.addObserver(self, selector: #selector(self.onOrientationChanged), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
self.panGestureRecognizer.delegate = gestureRecognizerDelegate
self.pinchGestureRecognizer.delegate = gestureRecognizerDelegate
self.rotateGestureRecognizer.delegate = gestureRecognizerDelegate
ApplicationManager.sharedInstance.gotFloorData = drawFloor
ApplicationManager.sharedInstance.currentUserPoint = drawCurrentUserPoint
}
func drawFloor (floor: Floor) {
mapView.setFloor(currentFloor: floor)
mapView.setNeedsDisplay()
}
func drawCurrentUserPoint(currentUserPoint: CurrentUserLocation, beaconRangingData: [BeaconRangingPoint]) {
mapView.setUserPoint(currentUserPoint: currentUserPoint, beaconRangingData: beaconRangingData)
mapView.setNeedsDisplay()
}
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
let translation = recognizer.translation(in: mapView)
if recognizer.view != nil {
let offsetX = translation.x * CGFloat(cosf(Float(rotateView))) - translation.y * CGFloat(sinf(Float(rotateView)))
let offsetY = translation.x * CGFloat(sinf(Float(rotateView))) + translation.y * CGFloat(cosf(Float(rotateView)))
mapView.center = CGPoint(x:mapView.center.x + offsetX * scaleView,
y:mapView.center.y + offsetY * scaleView)
}
recognizer.setTranslation(CGPoint(x: 0, y: 0), in: mapView)
}
#IBAction func handlePinch(recognizer : UIPinchGestureRecognizer) {
if recognizer.view != nil {
setAnchor(point: recognizer.location(in: mapView))
mapView.transform = mapView.transform.scaledBy(x: recognizer.scale, y: recognizer.scale)
scaleView = recognizer.scale * scaleView
recognizer.scale = 1
}
}
#IBAction func handleRotate(recognizer : UIRotationGestureRecognizer) {
if recognizer.view != nil {
setAnchor(point: recognizer.location(in: mapView))
mapView.transform = mapView.transform.rotated(by: recognizer.rotation)
rotateView = rotateView + recognizer.rotation
recognizer.rotation = 0
}
}
private func setAnchor(point : CGPoint) {
let anchor = CGPoint(x: point.x / mapView.bounds.width, y: point.y / mapView.bounds.height)
mapView.layer.anchorPoint = CGPoint(x: anchor.x, y: anchor.y)
let translationX = (mapView.bounds.width * (anchor.x - anchorPoint.x)) * scaleView
let translationY = (mapView.bounds.height * (anchor.y - anchorPoint.y)) * scaleView
let offsetX = translationX * CGFloat(cosf(Float(rotateView))) - translationY * CGFloat(sinf(Float(rotateView)))
let offsetY = translationX * CGFloat(sinf(Float(rotateView))) + translationY * CGFloat(cosf(Float(rotateView)))
mapView.layer.position = CGPoint(x: mapView.layer.position.x + offsetX,
y: mapView.layer.position.y + offsetY)
anchorPoint = anchor
}
private func showAlert(title: String, message: String?, style: UIAlertControllerStyle = .alert) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: style)
let tryAgainAction = UIAlertAction(title: "Try again", style: .default) {
alertAction in
ApplicationManager.sharedInstance.onApplicationStart()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil)
alertController.addAction(tryAgainAction)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
func onOrientationChanged() {
self.mapView.setNeedsDisplay()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
also code of mapView:
import UIKit
class MapView: UIView {
private var floor = Floor(walls: [], doors: [], beacons: [])
private let wallColor = UIColor.black
private let doorColor = UIColor.red
private let triangleColor = UIColor.red
private let perpendicularColor = UIColor.darkGray
private let doorLength = 3.0
private let beaconColor = UIColor.green
private let beaconNoActiveColor = UIColor(red: 20/255.0, green: 154.0/255.0, blue: 53.0/255.0, alpha: 1.0)
private let beaconFrameColor = UIColor.brown
private let lineWidthBeaconFrame:CGFloat = 0.25
private let beaconRadius: CGFloat = 5.0
private let userColor = UIColor.blue
private let userFrameColor = UIColor.brown
private let lineWidthUserFrame:CGFloat = 0.25
private let userRadius: CGFloat = 5.0
private let distanceBeaconColor = UIColor.clear
private let distanceBeaconFrameColor = UIColor.green
private let lineWidthOfDistanceFrame: CGFloat = 0.5
private let userRawRadius: CGFloat = 3.0
private let userRawColor = UIColor.darkGray
private let userRawFrameColor = UIColor.lightGray
private let lineWidthOfuserRawFrame: CGFloat = 0.8
private var beaconText: NSString = ""
private let textColor: UIColor = UIColor.red
private let textFont: UIFont = UIFont(name: "Helvetica Neue", size: 5)!
private var currentUserLocation = CurrentUserLocation()
private var beaconRangingData: [BeaconRangingPoint] = []
func setFloor (currentFloor: Floor) {
floor = currentFloor
}
func setUserPoint(currentUserPoint: CurrentUserLocation, beaconRangingData: [BeaconRangingPoint]) {
self.currentUserLocation = currentUserPoint
self.beaconRangingData = beaconRangingData
}
func dropPin(location: CGPoint) {
}
override func draw(_ rect: CGRect) {
let frameToDraw = CoordinatesConverter(boundsWidth: bounds.width, boundsHeight: bounds.height, paddingX: 5, paddingY: 5)
let mapWithScaleCoordinaates = frameToDraw.getSuitableCoordinates(floor: floor, currentUserLocation: currentUserLocation, beaconRangingData: beaconRangingData)
let lines = mapWithScaleCoordinaates.lines
let circles = mapWithScaleCoordinaates.circles
drawLines(lines: lines)
drawCircles(circles: circles)
}
private func drawLines(lines :[Line]) {
let wallPath = UIBezierPath()
let doorPath = UIBezierPath()
let trianglePath = UIBezierPath()
let perpendicularPath = UIBezierPath()
for line in lines {
if line.type == .wall {
wallPath.move(to: CGPoint(x: line.x1, y: line.y1))
wallPath.addLine(to: CGPoint(x: line.x2, y: line.y2))
wallColor.setStroke()
wallPath.stroke()
}
if line.type == .door {
doorPath.move(to: CGPoint(x: line.x1, y: line.y1))
doorPath.addLine(to: CGPoint(x: line.x2, y: line.y2))
doorPath.lineWidth = CGFloat(doorLength)
doorColor.setStroke()
doorPath.stroke()
}
if line.type == .triangle {
trianglePath.move(to: CGPoint(x: line.x1, y: line.y1))
trianglePath.addLine(to: CGPoint(x: line.x2, y: line.y2))
triangleColor.setStroke()
trianglePath.stroke()
}
if line.type == .perpendicular {
perpendicularPath.move(to: CGPoint(x: line.x1, y: line.y1))
perpendicularPath.addLine(to: CGPoint(x: line.x2, y: line.y2))
perpendicularColor.setStroke()
perpendicularPath.stroke()
}
}
}
private func drawCircles(circles: [Circle]) {
let layerViews = layer.sublayers
if layerViews != nil {
for view in layerViews! {
if type(of: view) === CAShapeLayer.self {
view.removeFromSuperlayer()
}
}
}
for circle in circles {
if type(of: circle) === BeaconCircle.self {
if (circle as! BeaconCircle).correctedDistance == 0 {
infoToDrawCircle(circle: circle, radius: beaconRadius, color: beaconNoActiveColor.cgColor, frameColor: beaconFrameColor.cgColor, frameWidth: lineWidthBeaconFrame)
drawBeaconText(circle: circle as! BeaconCircle)
} else {
infoToDrawCircle(circle: circle, radius: beaconRadius, color: beaconColor.cgColor, frameColor: beaconFrameColor.cgColor, frameWidth: lineWidthBeaconFrame)
drawBeaconText(circle: circle as! BeaconCircle)
}
if type(of: circle) === BeaconCircle.self && (circle as! BeaconCircle).correctedDistance != 0 {
infoToDrawCircle(circle: circle, radius: CGFloat((circle as! BeaconCircle).correctedDistance), color: distanceBeaconColor.cgColor, frameColor: distanceBeaconFrameColor.cgColor, frameWidth: lineWidthOfDistanceFrame)
// infoToDrawCircle(circle: circle, radius: CGFloat((circle as! BeaconCircle).notCorrectedDistance), color: UIColor.gray.cgColor, frameColor: distanceBeaconFrameColor.cgColor, frameWidth: lineWidthOfDistanceFrame)
}
} else if type(of: circle) === UserCircle.self {
infoToDrawCircle(circle: circle, radius: userRadius, color: userColor.cgColor, frameColor: userFrameColor.cgColor, frameWidth: lineWidthUserFrame)
} else if type (of: circle) === UserRawCircle.self {
infoToDrawCircle(circle: circle, radius: userRawRadius, color: userRawColor.cgColor, frameColor: userRawFrameColor.cgColor, frameWidth: lineWidthOfuserRawFrame)
} else {
Logger.logMessage(message: "incorrect circle type", level: .error)
break
}
}
}
private func drawBeaconText (circle: BeaconCircle) {
let attributes: NSDictionary = [
NSForegroundColorAttributeName: textColor,
NSFontAttributeName: textFont
]
let minor = circle.minor
let correctDistance = Int(round(100 * circle.correctDistanceForText) / 100)
let notCorrectDistance = Int(round(100 * circle.notCorrectDistanceForText) / 100)
beaconText = "m: \(minor), cd: \(correctDistance), nd: \(notCorrectDistance)" as NSString
if circle.x > Int((bounds.width - 50)) {
beaconText.draw(at: CGPoint(x: circle.x - 70, y: circle.y + 5), withAttributes: attributes as? [String : Any])
} else if circle.x < 20 {
beaconText.draw(at: CGPoint(x: circle.x + 5, y: circle.y + 5), withAttributes: attributes as? [String : Any])
} else {
beaconText.draw(at: CGPoint(x: circle.x + 6, y: circle.y + 5), withAttributes: attributes as? [String : Any])
}
}
private func infoToDrawCircle (circle: Circle, radius: CGFloat, color: CGColor, frameColor: CGColor, frameWidth: CGFloat) {
let center = CGPoint(x: circle.x, y: circle.y)
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 360, clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = color
shapeLayer.lineWidth = frameWidth
shapeLayer.strokeColor = frameColor
layer.addSublayer(shapeLayer)
}
}
I suggest you conform your MapViewController to the MKMapViewDelegate. In this way you can add annotations (also animated) to the map and you won't have to worry about them anymore since it's all handled by the map delegate.
You can use:
self.map.showAnnotations(pins, animated: true)
where pins is an array of MKAnnotation. Here is the link to the documentation for the method.
Cheers

UIPanGestureRecognizer on subclass UIView

I've been trying to lean more about subclassing certain objects.
Now I've subclassed a UIView which has a PanGestureRecognizer for swiping left and right.
Can't seem to find the problem. It won't even move the UIView. I've tried looking the lifecycle of an UIView to set the isUserInteractionEnabled to true, but with no result. See the code below:
VC
import UIKit
class SwipeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
addNewProfile()
}
private func addNewProfile() {
let swipeView = SwiperView(frame: CGRect(x: self.view.bounds.width / 2 - 150, y: self.view.bounds.height / 2 - 75, width: 300, height: 150))
swipeView.parentView = self.view
swipeView.delegate = self
swipeView.shadow = true
swipeView.isUserInteractionEnabled = true
swipeView.backgroundColor = UIColor.white
swipeView.alpha = 0.0
view.addSubview(swipeView)
UIView.animate(withDuration: 0.3, animations: {
swipeView.alpha = 1.0
}, completion: { (succeed) in
swipeView.isUserInteractionEnabled = true
})
}
}
//MARK: - ChosenSwipeResultDelegate
extension SwipeViewController: ChosenSwipeResultDelegate {
func pickedLeftSide() {
}
func pickedRightSide() {
}
}
SwiperView
import UIKit
protocol ChosenSwipeResultDelegate {
func pickedLeftSide()
func pickedRightSide()
}
#IBDesignable class SwiperView: UIView {
private var _shadow: Bool!
private var _parentView: UIView!
var delegate: ChosenSwipeResultDelegate?
var parentView: UIView {
set {
_parentView = newValue
}
get {
return _parentView
}
}
#IBInspectable var shadow: Bool {
get {
return layer.shadowOpacity > 0.0
}
set {
if newValue == true {
addShadow()
}
}
}
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
if shadow == false {
layer.masksToBounds = true
}
}
}
override func setNeedsLayout() {
super.setNeedsLayout()
isUserInteractionEnabled = true
}
override func awakeFromNib() {
super.awakeFromNib()
isUserInteractionEnabled = true
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(SwiperView.dragging(gesture:)))
addGestureRecognizer(dragGesture)
}
func dragging(gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: parentView)
let tinderView = gesture.view!
tinderView.center = CGPoint(x: parentView.bounds.width / 2 + translation.x, y: parentView.bounds.height / 2 + translation.y)
let xFromCenter = tinderView.center.x - parentView.bounds.width / 2
let scale = min(100 / abs(xFromCenter), 1)
var rotation = CGAffineTransform(rotationAngle: xFromCenter / 200)
let stretch = rotation.scaledBy(x: scale, y: scale)
tinderView.transform = stretch
if gesture.state == .ended {
if tinderView.center.x < 100 {
print("left")
UIView.animate(withDuration: 0.3, animations: {
tinderView.alpha = 0.0
}, completion: { (succeed) in
self.delegate?.pickedLeftSide()
})
} else if tinderView.center.x > parentView.bounds.width - 100 {
print("right")
UIView.animate(withDuration: 0.3, animations: {
tinderView.alpha = 0.0
}, completion: { (succeed) in
self.delegate?.pickedRightSide()
})
} else {
print("Not chosen")
rotation = CGAffineTransform(rotationAngle: 0)
let stretch = rotation.scaledBy(x: 1, y: 1)
tinderView.transform = stretch
tinderView.center = CGPoint(x: parentView.bounds.width / 2, y: parentView.bounds.height / 2)
}
}
}
private func addShadow(shadowColor: CGColor = UIColor.black.cgColor, shadowOffset: CGSize = CGSize(width: 1.0, height: 2.0), shadowOpacity: Float = 0.4, shadowRadius: CGFloat = 3.0) {
layer.shadowColor = shadowColor
layer.shadowOffset = shadowOffset
layer.shadowOpacity = shadowOpacity
layer.shadowRadius = shadowRadius
}
}
add these code to your SwiperView see if it solves the problem
override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(SwiperView.dragging(gesture:)))
addGestureRecognizer(dragGesture)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isUserInteractionEnabled = true
let dragGesture = UIPanGestureRecognizer(target: self, action: #selector(SwiperView.dragging(gesture:)))
addGestureRecognizer(dragGesture)
}

Resources