The app I'm working on uses collection view cells to display data to the user. I want the user to be able to share the data that's contained in the cells, but there are usually too many cells to try to re-size and fit onto a single iPhone-screen-sized window and get a screenshot.
So the problem I'm having is trying to get an image of all the cells in a collection view, both on-screen and off-screen. I'm aware that off-screen cells don't actually exist, but I'd be interested in a way to kind of fake an image and draw in the data (if that's possible in swift).
In short, is there a way to programmatically create an image from a collection view and the cells it contains, both on and off screen with Swift?
Update
If memory is not a concern :
mutating func screenshot(scale: CGFloat) -> UIImage {
let currentSize = frame.size
let currentOffset = contentOffset // temp store current offset
frame.size = contentSize
setContentOffset(CGPointZero, animated: false)
// it might need a delay here to allow loading data.
let rect = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
self.drawViewHierarchyInRect(rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
frame.size = currentSize
setContentOffset(currentOffset, animated: false)
return resizeUIImage(image, scale: scale)
}
This works for me:
github link -> contains up to date code
getScreenshotRects creates the offsets to which to scroll and the frames to capture. (naming is not perfect)
takeScreenshotAtPoint scrolls to the point, sets a delay to allow a redraw, takes the screenshot and returns this via completion handler.
stitchImages creates a rect with the same size as the content and draws all images in them.
makeScreenshots uses the didSet on a nested array of UIImage and a counter to create all images while also waiting for completion. When this is done it fires it own completion handler.
Basic parts :
scroll collectionview -> works
take screenshot with delay for a redraw -> works
crop images that are overlapping -> apparently not needed
stitch all images -> works
basic math -> works
maybe freeze screen or hide when all this is happening (this is not in my answer)
Code :
protocol ScrollViewImager {
var bounds : CGRect { get }
var contentSize : CGSize { get }
var contentOffset : CGPoint { get }
func setContentOffset(contentOffset: CGPoint, animated: Bool)
func drawViewHierarchyInRect(rect: CGRect, afterScreenUpdates: Bool) -> Bool
}
extension ScrollViewImager {
func screenshot(completion: (screenshot: UIImage) -> Void) {
let pointsAndFrames = getScreenshotRects()
let points = pointsAndFrames.points
let frames = pointsAndFrames.frames
makeScreenshots(points, frames: frames) { (screenshots) -> Void in
let stitched = self.stitchImages(images: screenshots, finalSize: self.contentSize)
completion(screenshot: stitched!)
}
}
private func makeScreenshots(points:[[CGPoint]], frames : [[CGRect]],completion: (screenshots: [[UIImage]]) -> Void) {
var counter : Int = 0
var images : [[UIImage]] = [] {
didSet {
if counter < points.count {
makeScreenshotRow(points[counter], frames : frames[counter]) { (screenshot) -> Void in
counter += 1
images.append(screenshot)
}
} else {
completion(screenshots: images)
}
}
}
makeScreenshotRow(points[counter], frames : frames[counter]) { (screenshot) -> Void in
counter += 1
images.append(screenshot)
}
}
private func makeScreenshotRow(points:[CGPoint], frames : [CGRect],completion: (screenshots: [UIImage]) -> Void) {
var counter : Int = 0
var images : [UIImage] = [] {
didSet {
if counter < points.count {
takeScreenshotAtPoint(point: points[counter]) { (screenshot) -> Void in
counter += 1
images.append(screenshot)
}
} else {
completion(screenshots: images)
}
}
}
takeScreenshotAtPoint(point: points[counter]) { (screenshot) -> Void in
counter += 1
images.append(screenshot)
}
}
private func getScreenshotRects() -> (points:[[CGPoint]], frames:[[CGRect]]) {
let vanillaBounds = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
let xPartial = contentSize.width % bounds.size.width
let yPartial = contentSize.height % bounds.size.height
let xSlices = Int((contentSize.width - xPartial) / bounds.size.width)
let ySlices = Int((contentSize.height - yPartial) / bounds.size.height)
var currentOffset = CGPoint(x: 0, y: 0)
var offsets : [[CGPoint]] = []
var rects : [[CGRect]] = []
var xSlicesWithPartial : Int = xSlices
if xPartial > 0 {
xSlicesWithPartial += 1
}
var ySlicesWithPartial : Int = ySlices
if yPartial > 0 {
ySlicesWithPartial += 1
}
for y in 0..<ySlicesWithPartial {
var offsetRow : [CGPoint] = []
var rectRow : [CGRect] = []
currentOffset.x = 0
for x in 0..<xSlicesWithPartial {
if y == ySlices && x == xSlices {
let rect = CGRect(x: bounds.width - xPartial, y: bounds.height - yPartial, width: xPartial, height: yPartial)
rectRow.append(rect)
} else if y == ySlices {
let rect = CGRect(x: 0, y: bounds.height - yPartial, width: bounds.width, height: yPartial)
rectRow.append(rect)
} else if x == xSlices {
let rect = CGRect(x: bounds.width - xPartial, y: 0, width: xPartial, height: bounds.height)
rectRow.append(rect)
} else {
rectRow.append(vanillaBounds)
}
offsetRow.append(currentOffset)
if x == xSlices {
currentOffset.x = contentSize.width - bounds.size.width
} else {
currentOffset.x = currentOffset.x + bounds.size.width
}
}
if y == ySlices {
currentOffset.y = contentSize.height - bounds.size.height
} else {
currentOffset.y = currentOffset.y + bounds.size.height
}
offsets.append(offsetRow)
rects.append(rectRow)
}
return (points:offsets, frames:rects)
}
private func takeScreenshotAtPoint(point point_I: CGPoint, completion: (screenshot: UIImage) -> Void) {
let rect = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.bounds.size.height)
let currentOffset = contentOffset
setContentOffset(point_I, animated: false)
delay(0.001) {
UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
self.drawViewHierarchyInRect(rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.setContentOffset(currentOffset, animated: false)
completion(screenshot: image)
}
}
private func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
private func crop(image image_I:UIImage, toRect rect:CGRect) -> UIImage? {
guard let imageRef: CGImageRef = CGImageCreateWithImageInRect(image_I.CGImage, rect) else {
return nil
}
return UIImage(CGImage:imageRef)
}
private func stitchImages(images images_I: [[UIImage]], finalSize : CGSize) -> UIImage? {
let finalRect = CGRect(x: 0, y: 0, width: finalSize.width, height: finalSize.height)
guard images_I.count > 0 else {
return nil
}
UIGraphicsBeginImageContext(finalRect.size)
var offsetY : CGFloat = 0
for imageRow in images_I {
var offsetX : CGFloat = 0
for image in imageRow {
let width = image.size.width
let height = image.size.height
let rect = CGRect(x: offsetX, y: offsetY, width: width, height: height)
image.drawInRect(rect)
offsetX += width
}
offsetX = 0
if let firstimage = imageRow.first {
offsetY += firstimage.size.height
} // maybe add error handling here
}
let stitchedImages = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return stitchedImages
}
}
extension UIScrollView : ScrollViewImager {
}
Draw the bitmap data of your UICollectionView into a UIImage using UIKit graphics functions. Then you'll have a UIImage that you could save to disk or do whatever you need with it. Something like this should work:
// your collection view
#IBOutlet weak var myCollectionView: UICollectionView!
//...
let image: UIImage!
// draw your UICollectionView into a UIImage
UIGraphicsBeginImageContext(myCollectionView.frame.size)
myCollectionView.layer.renderInContext(UIGraphicsGetCurrentContext()!)
image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
For swift 4 to make screenshot of UICollectionView
func makeScreenShotToShare()-> UIImage{
UIGraphicsBeginImageContextWithOptions(CGSize.init(width: self.colHistory.contentSize.width, height: self.colHistory.contentSize.height + 84.0), false, 0)
colHistory.scrollToItem(at: IndexPath.init(row: 0, section: 0), at: .top, animated: false)
colHistory.layer.render(in: UIGraphicsGetCurrentContext()!)
let row = colHistory.numberOfItems(inSection: 0)
let numberofRowthatShowinscreen = self.colHistory.size.height / (self.arrHistoryData.count == 1 ? 130 : 220)
let scrollCount = row / Int(numberofRowthatShowinscreen)
for i in 0..<scrollCount {
colHistory.scrollToItem(at: IndexPath.init(row: (i+1)*Int(numberofRowthatShowinscreen), section: 0), at: .top, animated: false)
colHistory.layer.render(in: UIGraphicsGetCurrentContext()!)
}
let image:UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext();
return image
}
Related
I am trying to build something for my own learning where I select an image from my photo library then divide that image up into sections. I had found info on how to split a single UIImage into sections, but in order to do that I need to have access to the cgImage property of the UIImage object. My problem is, the cgImage is always nil/null when selecting an image from the UIImagePickerController. Below is a stripped down version of my code, I'm hoping someone knows why the cgImage is always nil/null...
class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
#IBOutlet weak var selectButton: UIButton!
let picker = UIImagePickerController()
var image: UIImage!
var images: [UIImage]!
override func viewDidLoad() {
super.viewDidLoad()
picker.delegate = self
picker.sourceType = .photoLibrary
}
#objc func selectPressed(_ sender: UIButton) {
self.present(picker, animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info [UIImagePickerController.InfoKey : Any]) {
guard let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else {
self.picker.dismiss(animated: true, completion: nil)
return
}
self.image = image
self.picker.dismiss(animated: true, completion: nil)
self.makePuzzle()
}
func makePuzze() {
let images = self.image.split(times: 5)
}
}
extension UIImage {
func split(times: Int) -> [UIImage] {
let size = self.size
var xpos = 0, ypos = 0
var images: [UIImage] = []
let width = Int(size.width) / times
let height = Int(size.height) / times
for x in 0..<times {
xpos = 0
for y in 0..<times {
let rect = CGRect(x: xpos, y: ypos, width: width, height: height)
let ciRef = self.cgImage?.cropping(to: rect) //this is always nil
let img = UIImage(cgImage: ciRef!) //crash because nil
xpos += width
images.append(img)
}
ypos += height
}
return images
}
}
I can't seem to get the cgImage to be anything but nil/null and the app crashes every time. I know I can change the ! to ?? nil or something similar to avoid the crash, or add a guard or something, but that isn't really the problem, the problem is the cgImage is nil. I have looked around and the only thing I can find is how to get the cgImage with something like image.cgImage but that doesn't work. I think it has something to do with the image being selected from the UIImagePickerController, maybe that doesn't create the cgImage properly? Honestly not sure and could use some help. Thank you.
This is not an answer, just a beefed up comment with code.
Your assumption that the problem may be due to the UIImagePickerController could be correct.
Here is my SwiftUI test code. It shows your split(..) code (with some minor mods) working.
extension UIImage {
func split(times: Int) -> [UIImage] {
let size = self.size
var xpos = 0, ypos = 0
var images: [UIImage] = []
let width = Int(size.width) / times
let height = Int(size.height) / times
if let cgimg = self.cgImage { // <-- here
for _ in 0..<times {
xpos = 0
for _ in 0..<times {
let rect = CGRect(x: xpos, y: ypos, width: width, height: height)
if let ciRef = cgimg.cropping(to: rect) { // <-- here
let img = UIImage(cgImage: ciRef)
xpos += width
images.append(img)
}
}
ypos += height
}
}
return images
}
}
struct ContentView: View {
#State var imgSet = [UIImage]()
var body: some View {
ScrollView {
ForEach(imgSet, id: \.self) { img in
Image(uiImage: img).resizable().frame(width: 100, height: 100)
}
}
.onAppear {
if let img = UIImage(systemName: "globe") { // for testing
imgSet = img.split(times: 2)
}
}
}
}
I want to hide my collectionView's Header cell when scrolled up. And show it again once the user scroll's down a bit. I've tried it using UICollectionViewFlowLayout but with no success.
class FilterHeaderLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
var safeAreaTop = 0.0
if #available(iOS 13.0, *) {
let window = UIApplication.shared.windows.first
safeAreaTop = window!.safeAreaInsets.top
}
layoutAttributes?.forEach({ attributes in
if attributes.representedElementKind == UICollectionView.elementKindSectionHeader{
guard let collectionView = collectionView else { return }
let width = collectionView.frame.width
let contentOfsetY = collectionView.contentOffset.y
if contentOfsetY < 0 {
//prevents header cell to drift away if user scroll all the way down
// 41 is height of a view in navigation bar. header cell needs to be below navigation bar.
attributes.frame = .init(x: 0, y: safeAreaTop+41+contentOfsetY, width: width, height: 50)
return
}
let scrollVelocity = collectionView.panGestureRecognizer.velocity(in: collectionView.superview)
if (scrollVelocity.y > 0.0) {
// show the header and make it stay at top
} else if (scrollVelocity.y < 0.0) {
// hide the header
}
attributes.frame = .init(x: 0, y: safeAreaTop+41+contentOfsetY, width: width, height: 50)
}
})
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
that's code is very old, but let's try:
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint) -> CGPoint {
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let verticalOffset = proposedContentOffset.y
let targetRect = CGRect(origin: CGPoint(x: 0, y: proposedContentOffset.y), size: self.collectionView!.bounds.size)
for layoutAttributes in super.layoutAttributesForElements(in: targetRect)! {
let itemOffset = layoutAttributes.frame.origin.y
if (abs(itemOffset - verticalOffset) < abs(offsetAdjustment)) {
offsetAdjustment = itemOffset - verticalOffset
}
}
return CGPoint(x: proposedContentOffset.x, y: proposedContentOffset.y + offsetAdjustment)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesArray = super.layoutAttributesForElements(in: rect)
attributesArray?.forEach({ (attributes) in
var length: CGFloat = 0
var contentOffset: CGFloat = 0
var position: CGFloat = 0
let collectionView = self.collectionView!
if (self.scrollDirection == .horizontal) {
length = attributes.size.width
contentOffset = collectionView.contentOffset.x
position = attributes.center.x - attributes.size.width / 2
} else {
length = attributes.size.height
contentOffset = collectionView.contentOffset.y
position = attributes.center.y - attributes.size.height / 2
}
if (position >= 0 && position <= contentOffset) {
let differ: CGFloat = contentOffset - position
let alphaFactor: CGFloat = 1 - differ / length
attributes.alpha = alphaFactor
attributes.transform3D = CATransform3DMakeTranslation(0, differ, 0)
} else if (position - contentOffset > collectionView.frame.height - Layout.model.height - Layout.model.spacing
&& position - contentOffset <= collectionView.frame.height) {
let differ: CGFloat = collectionView.frame.height - position + contentOffset
let alphaFactor: CGFloat = differ / length
attributes.alpha = alphaFactor
attributes.transform3D = CATransform3DMakeTranslation(0, differ - length, 0)
attributes.zIndex = -1
} else {
attributes.alpha = 1
attributes.transform = CGAffineTransform.identity
}
})
return attributesArray
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
I am trying to crop a selected portion of NSImage which is fitted as per ProportionallyUpOrDown(AspectFill) Mode.
I am drawing a frame using mouse dragged event like this:
class CropImageView: NSImageView {
var startPoint: NSPoint!
var shapeLayer: CAShapeLayer!
var flagCheck = false
var finalPoint: NSPoint!
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
override var image: NSImage? {
set {
self.layer = CALayer()
self.layer?.contentsGravity = kCAGravityResizeAspectFill
self.layer?.contents = newValue
self.wantsLayer = true
super.image = newValue
}
get {
return super.image
}
}
override func mouseDown(with event: NSEvent) {
self.startPoint = self.convert(event.locationInWindow, from: nil)
if self.shapeLayer != nil {
self.shapeLayer.removeFromSuperlayer()
self.shapeLayer = nil
}
self.flagCheck = true
var pixelColor: NSColor = NSReadPixel(startPoint) ?? NSColor()
shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 1.0
shapeLayer.fillColor = NSColor.clear.cgColor
if pixelColor == NSColor.black {
pixelColor = NSColor.color_white
} else {
pixelColor = NSColor.black
}
shapeLayer.strokeColor = pixelColor.cgColor
shapeLayer.lineDashPattern = [1]
self.layer?.addSublayer(shapeLayer)
var dashAnimation = CABasicAnimation()
dashAnimation = CABasicAnimation(keyPath: "lineDashPhase")
dashAnimation.duration = 0.75
dashAnimation.fromValue = 0.0
dashAnimation.toValue = 15.0
dashAnimation.repeatCount = 0.0
shapeLayer.add(dashAnimation, forKey: "linePhase")
}
override func mouseDragged(with event: NSEvent) {
let point: NSPoint = self.convert(event.locationInWindow, from: nil)
var newPoint: CGPoint = self.startPoint
let xDiff = point.x - self.startPoint.x
let yDiff = point.y - self.startPoint.y
let dist = min(abs(xDiff), abs(yDiff))
newPoint.x += xDiff > 0 ? dist : -dist
newPoint.y += yDiff > 0 ? dist : -dist
let path = CGMutablePath()
path.move(to: self.startPoint)
path.addLine(to: NSPoint(x: self.startPoint.x, y: newPoint.y))
path.addLine(to: newPoint)
path.addLine(to: NSPoint(x: newPoint.x, y: self.startPoint.y))
path.closeSubpath()
self.shapeLayer.path = path
}
override func mouseUp(with event: NSEvent) {
self.finalPoint = self.convert(event.locationInWindow, from: nil)
}
}
and selected this area as shown in picture using black dotted line:
My Cropping Code logic is this:
// resize Image Methods
extension CropProfileView {
func resizeImage(image: NSImage) -> Data {
var scalingFactor: CGFloat = 0.0
if image.size.width >= image.size.height {
scalingFactor = image.size.width/cropImgView.size.width
} else {
scalingFactor = image.size.height/cropImgView.size.height
}
let width = (self.cropImgView.finalPoint.x - self.cropImgView.startPoint.x) * scalingFactor
let height = (self.cropImgView.startPoint.y - self.cropImgView.finalPoint.y) * scalingFactor
let xPos = ((image.size.width/2) - (cropImgView.bounds.midX - self.cropImgView.startPoint.x) * scalingFactor)
let yPos = ((image.size.height/2) - (cropImgView.bounds.midY - (cropImgView.size.height - self.cropImgView.startPoint.y)) * scalingFactor)
var croppedRect: NSRect = NSRect(x: xPos, y: yPos, width: width, height: height)
let imageRef = image.cgImage(forProposedRect: &croppedRect, context: nil, hints: nil)
guard let croppedImage = imageRef?.cropping(to: croppedRect) else {return Data()}
let imageWithNewSize = NSImage(cgImage: croppedImage, size: NSSize(width: width, height: height))
guard let data = imageWithNewSize.tiffRepresentation,
let rep = NSBitmapImageRep(data: data),
let imgData = rep.representation(using: .png, properties: [.compressionFactor: NSNumber(floatLiteral: 0.25)]) else {
return imageWithNewSize.tiffRepresentation ?? Data()
}
return imgData
}
}
With this cropping logic i am getting this output:
I think as image is AspectFill thats why its not getting cropped in perfect size as per selected frame. Here if you look at output: xpositon & width & heights are not perfect. Or probably i am not calculating these co-ordinates properly. Let me know the faults probably i am calculating someting wrong.
Note: the CropImageView class in the question is a subclass of NSImageView but the view is layer-hosting and the image is drawn by the layer, not by NSImageView. imageScaling is not used.
When deciding which scaling factor to use you have to take the size of the image view into account. If the image size is width:120, height:100 and the image view size is width:120, height 80 then image.size.width >= image.size.height is true and image.size.width/cropImgView.size.width is 1 but the image is scaled because image.size.height/cropImgView.size.height is 1.25. Calculate the horizontal and vertical scaling factors and use the largest.
See How to crop a UIImageView to a new UIImage in 'aspect fill' mode?
Here's the calculation of croppedRect assuming cropImgView.size returns self.layer!.bounds.size.
var scalingWidthFactor: CGFloat = image.size.width/cropImgView.size.width
var scalingHeightFactor: CGFloat = image.size.height/cropImgView.size.height
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
switch cropImgView.layer?.contentsGravity {
case CALayerContentsGravity.resize: break
case CALayerContentsGravity.resizeAspect:
if scalingWidthFactor > scalingHeightFactor {
scalingHeightFactor = scalingWidthFactor
yOffset = (cropImgView.size.height - (image.size.height / scalingHeightFactor)) / 2
}
else {
scalingWidthFactor = scalingHeightFactor
xOffset = (cropImgView.size.width - (image.size.width / scalingWidthFactor)) / 2
}
case CALayerContentsGravity.resizeAspectFill:
if scalingWidthFactor < scalingHeightFactor {
scalingHeightFactor = scalingWidthFactor
yOffset = (cropImgView.size.height - (image.size.height / scalingHeightFactor)) / 2
}
else {
scalingWidthFactor = scalingHeightFactor
xOffset = (cropImgView.size.width - (image.size.width / scalingWidthFactor)) / 2
}
default:
print("contentsGravity \(String(describing: cropImgView.layer?.contentsGravity)) is not supported")
return nil
}
let width = (self.cropImgView.finalPoint.x - self.cropImgView.startPoint.x) * scalingWidthFactor
let height = (self.cropImgView.startPoint.y - self.cropImgView.finalPoint.y) * scalingHeightFactor
let xPos = (self.cropImgView.startPoint.x - xOffset) * scalingWidthFactor
let yPos = (cropImgView.size.height - self.cropImgView.startPoint.y - yOffset) * scalingHeightFactor
var croppedRect: NSRect = NSRect(x: xPos, y: yPos, width: width, height: height)
Bugfix: cropImgView.finalPoint should be the corner of the selection, not the location of mouseUp. In CropImageView set self.finalPoint = newPoint in mouseDragged instead of mouseUp.
I tried to test a waving animated UIView that is runloop based on SwiftUI using ''UIViewRepresentable'' but it does not appear to be animating at all.
Using UIViewRepresentable Protocol to connect swiftui to UIView.
Swift UI Code:
import SwiftUI
struct WaveView: UIViewRepresentable {
func makeUIView(context: Context) -> WaveUIView {
WaveUIView(frame: .init(x: 0, y: 0, width: 300, height: 300))
}
func updateUIView(_ view: WaveUIView, context: Context) {
view.start()
}
}
struct WaveView_Previews: PreviewProvider {
static var previews: some View {
WaveView()
}
}
The "Waving" UIView that I tested working on UIViewController way of doing it.
import Foundation
import UIKit
class WaveUIView:UIView {
/// wave curvature (default: 1.5)
open var waveCurvature: CGFloat = 1.5
/// wave speed (default: 0.6)
open var waveSpeed: CGFloat = 0.6
/// wave height (default: 5)
open var waveHeight: CGFloat = 5
/// real wave color
open var realWaveColor: UIColor = UIColor.red {
didSet {
self.realWaveLayer.fillColor = self.realWaveColor.cgColor
}
}
/// mask wave color
open var maskWaveColor: UIColor = UIColor.red {
didSet {
self.maskWaveLayer.fillColor = self.maskWaveColor.cgColor
}
}
/// float over View
open var overView: UIView?
/// wave timmer
fileprivate var timer: CADisplayLink?
/// real aave
fileprivate var realWaveLayer :CAShapeLayer = CAShapeLayer()
/// mask wave
fileprivate var maskWaveLayer :CAShapeLayer = CAShapeLayer()
/// offset
fileprivate var offset :CGFloat = 0
fileprivate var _waveCurvature: CGFloat = 0
fileprivate var _waveSpeed: CGFloat = 0
fileprivate var _waveHeight: CGFloat = 0
fileprivate var _starting: Bool = false
fileprivate var _stoping: Bool = false
/**
Init view
- parameter frame: view frame
- returns: view
*/
override init(frame: CGRect) {
super.init(frame: frame)
var frame = self.bounds
frame.origin.y = frame.size.height
frame.size.height = 0
maskWaveLayer.frame = frame
realWaveLayer.frame = frame
// test
self.backgroundColor = UIColor.blue
}
/**
Init view with wave color
- parameter frame: view frame
- parameter color: real wave color
- returns: view
*/
public convenience init(frame: CGRect, color:UIColor) {
self.init(frame: frame)
self.realWaveColor = color
self.maskWaveColor = color.withAlphaComponent(0.4)
realWaveLayer.fillColor = self.realWaveColor.cgColor
maskWaveLayer.fillColor = self.maskWaveColor.cgColor
self.layer.addSublayer(self.realWaveLayer)
self.layer.addSublayer(self.maskWaveLayer)
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/**
Add over view
- parameter view: overview
*/
open func addOverView(_ view: UIView) {
overView = view
overView?.center = self.center
overView?.frame.origin.y = self.frame.height - (overView?.frame.height)!
self.addSubview(overView!)
}
/**
Start wave
*/
open func start() {
if !_starting {
_stop()
_starting = true
_stoping = false
_waveHeight = 0
_waveCurvature = 0
_waveSpeed = 0
timer = CADisplayLink(target: self, selector: #selector(wave))
timer?.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
}
}
/**
Stop wave
*/
open func _stop(){
if (timer != nil) {
timer?.invalidate()
timer = nil
}
}
open func stop(){
if !_stoping {
_starting = false
_stoping = true
}
}
/**
Wave animation
*/
#objc func wave() {
// when view is not visible
// if overView?.window == nil {
// print("not playing cause not visible")
// return
// }
if _starting {
print("started")
if _waveHeight < waveHeight {
_waveHeight = _waveHeight + waveHeight/100.0
var frame = self.bounds
frame.origin.y = frame.size.height-_waveHeight
frame.size.height = _waveHeight
maskWaveLayer.frame = frame
realWaveLayer.frame = frame
_waveCurvature = _waveCurvature + waveCurvature / 100.0
_waveSpeed = _waveSpeed + waveSpeed / 100.0
} else {
_starting = false
}
}
if _stoping {
if _waveHeight > 0 {
_waveHeight = _waveHeight - waveHeight/50.0
var frame = self.bounds
frame.origin.y = frame.size.height
frame.size.height = _waveHeight
maskWaveLayer.frame = frame
realWaveLayer.frame = frame
_waveCurvature = _waveCurvature - waveCurvature / 50.0
_waveSpeed = _waveSpeed - waveSpeed / 50.0
} else {
_stoping = false
_stop()
}
}
offset += _waveSpeed
let width = frame.width
let height = CGFloat(_waveHeight)
let path = CGMutablePath()
path.move(to: CGPoint(x: 0, y: height))
var y: CGFloat = 0
let maskpath = CGMutablePath()
maskpath.move(to: CGPoint(x: 0, y: height))
let offset_f = Float(offset * 0.045)
let waveCurvature_f = Float(0.01 * _waveCurvature)
for x in 0...Int(width) {
y = height * CGFloat(sinf( waveCurvature_f * Float(x) + offset_f))
path.addLine(to: CGPoint(x: CGFloat(x), y: y))
maskpath.addLine(to: CGPoint(x: CGFloat(x), y: -y))
}
if (overView != nil) {
let centX = self.bounds.size.width/2
let centY = height * CGFloat(sinf(waveCurvature_f * Float(centX) + offset_f))
let center = CGPoint(x: centX , y: centY + self.bounds.size.height - overView!.bounds.size.height/2 - _waveHeight - 1 )
overView?.center = center
}
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.closeSubpath()
self.realWaveLayer.path = path
maskpath.addLine(to: CGPoint(x: width, y: height))
maskpath.addLine(to: CGPoint(x: 0, y: height))
maskpath.closeSubpath()
self.maskWaveLayer.path = maskpath
}
}
I expect the SwiftUI to have the view animating and correctly have the frame/border changes according animation. But it is not animating at all right now.
Following is the animated view with UIViewController:
override func viewWillAppear(_ animated: Bool) {
cardView.start()
}
func viewdidload(){
let frame = CGRect(x: 0, y: 0, width: self.view.bounds.size.width * 0.8, height: view.bounds.height * 0.5)
cardView = HomeCardView(frame: frame, color: .gray)
cardView.addOverView(someUIView())
cardView.realWaveColor = UIColor.white.withAlphaComponent(0.7)
cardView.maskWaveColor = UIColor.white.withAlphaComponent(0.3)
cardView.waveSpeed = 1.2
cardView.waveHeight = 10
view.addSubview(cardView)
}
You forget to addOverview in update uimethod
func updateUIView(_ view: WaveUIView, context: Context) {
let overView: UIView = UIView(frame: CGRect.init(x: 0, y: 0, width: 300, height: 300))
overView.backgroundColor = UIColor.green
view.addOverView(overView)
view.start()
}
I am experiencing this weird bug with my custom view. The custom view is supposed to show meters of rating distribution. It gets added to a cell view of an outline view.
When I resize the window, the custom view somehow gets squished and looks broken. I have pasted the drawRect of the custom view below.
override func drawRect(r: NSRect) {
super.drawRect(r)
var goodRect: NSRect?
var okRect: NSRect?
var badRect: NSRect?
let barHeight = CGFloat(10.0)
if self.goodPercent != 0.0 {
goodRect = NSRect(x: 0, y: 0, width: r.width * CGFloat(goodPercent), height: barHeight)
let goodPath = NSBezierPath(roundedRect: goodRect!, xRadius: 6, yRadius: 6)
RatingDistributionView.goodColor.setFill()
goodPath.fill()
}
if self.okPercent != 0.0 {
let okX = CGFloat(goodRect?.width ?? 0.0)
okRect = NSRect(x: okX, y: 0, width: r.width * CGFloat(okPercent), height: barHeight)
let okPath = NSBezierPath(roundedRect: okRect!, xRadius: 6, yRadius: 6)
RatingDistributionView.okColor.setFill()
okPath.fill()
}
if self.badPercent != 0.0 {
var badX: CGFloat
//Cases:
//Good persent and OK present - badX = okRect.x + okRect.width
//Good persent and OK missing - badX = goodRect.x + goodRect.width
//Good missing and OK present - badX = okRect.x + okRect.width
//Both missing -
if okRect != nil {
badX = okRect!.origin.x + okRect!.width
}else if goodRect != nil {
badX = goodRect!.origin.x + goodRect!.width
} else {
badX = 0.0
}
badRect = NSRect(x: badX, y: 0, width: r.width * CGFloat(badPercent), height: barHeight)
let badPath = NSBezierPath(roundedRect: badRect!, xRadius: 6, yRadius: 6)
RatingDistributionView.badColor.setFill()
badPath.fill()
}
//Draw dividers
let divWidth = CGFloat(6.75)
if self.goodPercent != 0.0 && (self.okPercent != 0.0 || self.badPercent != 0.0) {
let divX = goodRect!.origin.x + goodRect!.width
let divRect = NSRect(x: divX - (divWidth / 2.0), y: 0.0, width: divWidth, height: barHeight)
let divPath = NSBezierPath(roundedRect: divRect, xRadius: 0, yRadius: 0)
NSColor.whiteColor().setFill()
divPath.fill()
}
if self.okPercent != 0.0 && self.badPercent != 0.0 {
let divX = okRect!.origin.x + okRect!.width
let divRect = NSRect(x: divX - (divWidth / 2.0), y: 0.0, width: divWidth, height: barHeight)
let divPath = NSBezierPath(roundedRect: divRect, xRadius: 0, yRadius: 0)
NSColor.whiteColor().setFill()
divPath.fill()
}
}
AN alternative solution for your problem is to use NSView. You can have a container view with rounded corner and then drawing subviews (red, orange, green) in that container. like this;
I have written a class for it that you may customise according to your requirements;
public class CProgressView:NSView {
private lazy var goodView:NSView = {
let viw:NSView = NSView(frame: NSRect.zero);
viw.layer = CALayer();
viw.layer?.backgroundColor = NSColor.greenColor().CGColor;
self.addSubview(viw)
return viw;
} ();
private lazy var okView:NSView = {
let viw:NSView = NSView(frame: NSRect.zero);
viw.layer = CALayer();
viw.layer?.backgroundColor = NSColor.orangeColor().CGColor;
self.addSubview(viw)
return viw;
} ();
private lazy var badView:NSView = {
let viw:NSView = NSView(frame: NSRect.zero);
viw.layer = CALayer();
viw.layer?.backgroundColor = NSColor.redColor().CGColor;
self.addSubview(viw)
return viw;
} ();
private var _goodProgress:CGFloat = 33;
private var _okProgress:CGFloat = 33;
private var _badProgress:CGFloat = 34;
private var goodViewFrame:NSRect {
get {
let rect:NSRect = NSRect(x: 0, y: 0, width: (self.frame.size.width * (_goodProgress / 100.0)), height: self.frame.size.height);
return rect;
}
}
private var okViewFrame:NSRect {
get {
let rect:NSRect = NSRect(x: self.goodViewFrame.size.width, y: 0, width: (self.frame.size.width * (_okProgress / 100.0)), height: self.frame.size.height);
return rect;
}
}
private var badViewFrame:NSRect {
get {
let width:CGFloat = (self.frame.size.width * (_badProgress / 100.0));
let rect:NSRect = NSRect(x: self.frame.size.width - width, y: 0, width: width, height: self.frame.size.height);
return rect;
}
}
override public init(frame frameRect: NSRect) {
super.init(frame: frameRect);
//--
self.commonInit();
}
required public init?(coder: NSCoder) {
super.init(coder: coder);
}
override public func awakeFromNib() {
super.awakeFromNib();
//--
self.commonInit();
}
private func commonInit() {
self.layer = CALayer();
self.layer!.cornerRadius = 15;
self.layer!.masksToBounds = true
//-
self.updateFrames();
}
public func updateProgress(goodProgressV:Int, okProgressV:Int, badProgressV:Int) {
guard ((goodProgressV + okProgressV + badProgressV) == 100) else {
NSLog("Total should be 100%");
return;
}
_goodProgress = CGFloat(goodProgressV);
_okProgress = CGFloat(okProgressV);
_badProgress = CGFloat(badProgressV);
//--
self.updateFrames();
}
private func updateFrames() {
self.layer?.backgroundColor = NSColor.grayColor().CGColor;
self.goodView.frame = self.goodViewFrame;
self.okView.frame = self.okViewFrame;
self.badView.frame = self.badViewFrame;
}
public override func resizeSubviewsWithOldSize(oldSize: NSSize) {
super.resizeSubviewsWithOldSize(oldSize);
//--
self.updateFrames();
}
}
Note: Call updateProgress() method for changing progress default is 33, 33 & 34 (33+33+34 = 100);
You may also download a sample project from the link below;
http://www.filedropper.com/osxtest
From the documentation of drawRect(_ dirtyRect: NSRect):
dirtyRect:
A rectangle defining the portion of the view that requires redrawing. This rectangle usually represents the portion of the view that requires updating. When responsive scrolling is enabled, this rectangle can also represent a nonvisible portion of the view that AppKit wants to cache.
Don't use dirtyRect for your calculations, use self.bounds.