I have some code I can't change that expects to be able to draw at any time. It's the main() function in BackgroundThread below - pretend it can't be modified in any way. Running this will use 70-80% CPU.
If instead of running the thread I replicate what it is doing in View::draw() (i.e. draw 5000 white rectangles at random positions), this will use about 30% CPU.
Where's the difference coming from? Looking at Instruments, although the call stack is the same starting from CGContextFillRect, the View::draw() version only spends 16% of the time doing memset() whereas the threaded version spends 80% of the time.
The code below is the FAST version. Comment out the FAST lines and uncomment the SLOW lines to switch to the SLOW (threaded) version. Compile with swiftc test.swift -otest && ./test. I'm on macOS 10.13, integrated graphics, if that matters.
Is there anything I can do to make the threaded version as fast as the View::draw() version?
import Cocoa
let NSApp = NSApplication.shared,
vwaitSem = DispatchSemaphore(value: 0)
var
mainWindow: NSWindow?,
screen: CGContext?,
link: CVDisplayLink?
class View: NSView, CALayerDelegate {
var lastTime: CFTimeInterval = 0
override var acceptsFirstResponder: Bool {return true}
required init(coder aDecoder: NSCoder) {fatalError("This class does not support NSCoding")}
override func makeBackingLayer() -> CALayer {return CALayer()}
override init(frame: CGRect) {
super.init(frame: frame)
self.wantsLayer = true
self.layer?.contentsScale = 2.0
self.layer?.backgroundColor = CGColor(red:0, green:0, blue:0, alpha: 1)
self.layerContentsRedrawPolicy = NSView.LayerContentsRedrawPolicy.onSetNeedsDisplay // FAST
}
func draw(_ layer: CALayer, in ctx: CGContext) {
let now = CACurrentMediaTime(), timePassed = ((now-lastTime)*1000).rounded()
// NSLog("\(timePassed)")
lastTime = now
ctx.setFillColor(CGColor.white)
ctx.setStrokeColor(CGColor.white)
for _ in 0...5000 {
let rect = CGRect(x: CGFloat(arc4random_uniform(640)+1), y: CGFloat(arc4random_uniform(480)+1), width:6, height:6)
ctx.setFillColor(CGColor.white)
ctx.fill(rect)
}
}
}
func displayLinkOutputCallback(_ displayLink: CVDisplayLink, _ nowPtr: UnsafePointer<CVTimeStamp>,
_ outputTimePtr: UnsafePointer<CVTimeStamp>, _ flagsIn: CVOptionFlags, _ flagsOut: UnsafeMutablePointer<CVOptionFlags>,
_ displayLinkContext: UnsafeMutableRawPointer?) -> CVReturn {
DispatchQueue.main.async {
// mainWindow!.contentView!.layer!.contents = screen!.makeImage() // SLOW
mainWindow!.contentView!.display() // FAST
vwaitSem.signal()
}
return kCVReturnSuccess
}
class BackgroundThread: Thread {
var lastTime: CFTimeInterval = 0
override func main() {
while true {
let now = CACurrentMediaTime(), timePassed = ((now-lastTime)*1000).rounded()
// NSLog("\(timePassed)")
lastTime = now
screen?.clear(CGRect(x:0, y:0, width:640*2, height:480*2))
for _ in 0...5000 {
screen?.setFillColor(CGColor.white)
screen?.setStrokeColor(CGColor.white)
screen?.fill(CGRect(x: CGFloat(arc4random_uniform(640*2)+1), y: CGFloat(arc4random_uniform(480*2)+1), width: 6*2, height: 6*2))
}
vwaitSem.wait()
}
}
}
let width = 640, height = 480,
appMenuItem = NSMenuItem(),
quitMenuItem = NSMenuItem(title:"Quit",
action:#selector(NSApplication.terminate), keyEquivalent:"q"),
window = NSWindow(contentRect:NSMakeRect(0,0, CGFloat(width), CGFloat(height)),
styleMask:[.closable,.titled], backing:.buffered, defer:false),
colorProfile = ColorSyncProfileCreateWithDisplayID(0),
colorSpace = CGColorSpace(platformColorSpaceRef: colorProfile!.toOpaque()),
screen_ = CGContext(data: nil, width: Int(width)*2, height:Int(height)*2, bitsPerComponent:8, bytesPerRow: 0,
space: colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue),
backgroundThread = BackgroundThread()
NSApp.setActivationPolicy(NSApplication.ActivationPolicy.regular)
NSApp.mainMenu = NSMenu()
NSApp.mainMenu?.addItem(appMenuItem)
appMenuItem.submenu = NSMenu()
appMenuItem.submenu?.addItem(quitMenuItem)
window.cascadeTopLeft(from:NSMakePoint(20,20))
window.makeKeyAndOrderFront(nil)
window.contentView = View()
window.makeFirstResponder(window.contentView)
NSApp.activate(ignoringOtherApps:true)
mainWindow = window
screen = screen_
CVDisplayLinkCreateWithCGDisplay(CGMainDisplayID(), &link)
CVDisplayLinkSetOutputCallback(link!, displayLinkOutputCallback, UnsafeMutableRawPointer(Unmanaged.passUnretained(window).toOpaque()))
CVDisplayLinkStart(link!)
// backgroundThread.start() // SLOW
NSApp.run()
I misread the note in the documentation for makeImage() and thought it would not copy the data unless it really had to. Well, Instruments shows it does copy the data. Every single frame.
So I switched to Metal and now I can draw from the background thread with the same performance/CPU usage as with CGContext alone, with no copies as far as I can tell.
Here's some working code:
import Cocoa
import MetalKit
class View: MTKView {
var screen: CGContext?
var commandQueue: MTLCommandQueue?
var buffer: MTLBuffer?
var texture: MTLTexture?
var vwaitSem = DispatchSemaphore(value: 0)
var backgroundThread: Thread?
var allocationSize = 0
func alignUp(size: Int, align: Int) -> Int {return (size+(align-1)) & ~(align-1)}
override var acceptsFirstResponder: Bool {return true}
required init(coder aDecoder: NSCoder) {fatalError("This class does not support NSCoding")}
init() {super.init(frame: CGRect(x:0, y:0, width:0, height: 0), device: MTLCreateSystemDefaultDevice())}
override func viewDidMoveToWindow() {
layer?.contentsScale = NSScreen.main!.backingScaleFactor
let metalLayer = layer as! CAMetalLayer
let pixelRowAlignment = metalLayer.device!.minimumLinearTextureAlignment(for: metalLayer.pixelFormat)
let bytesPerRow = alignUp(size: Int(layer!.frame.width)*Int(layer!.contentsScale)*4, align: pixelRowAlignment)
let pagesize = Int(getpagesize())
var data: UnsafeMutableRawPointer? = nil
allocationSize = alignUp(size: bytesPerRow*Int(layer!.frame.height)*Int(layer!.contentsScale), align: pagesize)
posix_memalign(&data, pagesize, allocationSize)
let colorProfile = ColorSyncProfileCreateWithDisplayID(0),
colorSpace = CGColorSpace(platformColorSpaceRef: colorProfile!.toOpaque()),
screen_ = CGContext(data: data,
width: Int(layer!.frame.width)*Int(layer!.contentsScale),
height: Int(layer!.frame.height)*Int(layer!.contentsScale),
bitsPerComponent:8, bytesPerRow: bytesPerRow,
space: colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!,
buffer_ = metalLayer.device!.makeBuffer(bytesNoCopy: data!, length: allocationSize, options: .storageModeManaged,
deallocator: { pointer, length in free(self.screen!.data!) })!,
textureDescriptor = MTLTextureDescriptor()
textureDescriptor.pixelFormat = metalLayer.pixelFormat
textureDescriptor.width = screen_.width
textureDescriptor.height = screen_.height
textureDescriptor.storageMode = buffer_.storageMode
textureDescriptor.usage = MTLTextureUsage(rawValue: MTLTextureUsage.shaderRead.rawValue)
texture = buffer_.makeTexture(descriptor: textureDescriptor, offset: 0, bytesPerRow: screen_.bytesPerRow)
commandQueue = device?.makeCommandQueue()
screen = screen_
buffer = buffer_
backgroundThread = BackgroundThread(screen: screen!, vwaitSem: vwaitSem)
backgroundThread!.start()
}
override func draw(_ dirtyRect: NSRect) {
if let drawable = currentDrawable {
buffer!.didModifyRange(0..<allocationSize)
texture!.replace(region: MTLRegionMake2D(0,0, screen!.width, screen!.height),
mipmapLevel:0, slice:0, withBytes: screen!.data!, bytesPerRow: screen!.bytesPerRow, bytesPerImage: 0)
let commandBuffer = commandQueue!.makeCommandBuffer()!
let blitPass = commandBuffer.makeBlitCommandEncoder()!
blitPass.copy(from: texture!, sourceSlice:0, sourceLevel:0, sourceOrigin: MTLOrigin(x:0,y:0,z:0),
sourceSize: MTLSize(width:screen!.width, height:screen!.height, depth: 1),
to: drawable.texture, destinationSlice:0, destinationLevel:0, destinationOrigin: MTLOrigin(x:0,y:0,z:0))
blitPass.endEncoding()
if let renderPass = currentRenderPassDescriptor {
renderPass.colorAttachments[0].texture = drawable.texture
renderPass.colorAttachments[0].loadAction = .load
commandBuffer.makeRenderCommandEncoder(descriptor: renderPass)!.endEncoding()
commandBuffer.addCompletedHandler {cb in self.vwaitSem.signal()}
commandBuffer.present(drawable)
commandBuffer.commit()
}
}
}
}
class BackgroundThread: Thread {
var screen: CGContext
var vwaitSem: DispatchSemaphore
var x = 0
init(screen:CGContext, vwaitSem:DispatchSemaphore) {
self.screen = screen
self.vwaitSem = vwaitSem
}
override func main() {
while true {
// screen.clear(CGRect(x:0,y:0, width:screen.width, height:screen.height))
// screen.setFillColor(CGColor.white)
// screen.fill(CGRect(x:x, y:0, width:100, height:100))
// x += 1
screen.clear(CGRect(x:0,y:0, width:screen.width, height:screen.height))
screen.setFillColor(CGColor.white)
let screenWidth = UInt32(screen.width), screenHeight = UInt32(screen.height)
for _ in 0...5000 {
let rect = CGRect(x: CGFloat(arc4random_uniform(screenWidth+1)),
y: CGFloat(arc4random_uniform(screenHeight+1)), width:6, height:6)
screen.fill(rect)
}
vwaitSem.wait()
}
}
}
let width = 640, height = 480,
appMenuItem = NSMenuItem(),
quitMenuItem = NSMenuItem(title:"Quit",
action:#selector(NSApplication.terminate), keyEquivalent:"q"),
window = NSWindow(contentRect:NSMakeRect(0,0, CGFloat(width), CGFloat(height)),
styleMask:[.closable,.titled], backing:.buffered, defer:false)
NSApp.setActivationPolicy(NSApplication.ActivationPolicy.regular)
NSApp.mainMenu = NSMenu()
NSApp.mainMenu?.addItem(appMenuItem)
appMenuItem.submenu = NSMenu()
appMenuItem.submenu?.addItem(quitMenuItem)
window.cascadeTopLeft(from:NSMakePoint(20,20))
window.makeKeyAndOrderFront(nil)
window.contentView = View()
window.makeFirstResponder(window.contentView)
NSApp.activate(ignoringOtherApps:true)
NSApp.run()
Related
I am doing some task to apply filter effect in to my WebRTC call, follow this tutorial:
https://developer.apple.com/documentation/vision/applying_matte_effects_to_people_in_images_and_video
Here is my code to convert:
func capturer(_ capturer: RTCVideoCapturer, didCapture frame: RTCVideoFrame) {
let pixelBufferr = frame.buffer as! RTCCVPixelBuffer
let pixelBufferRef = pixelBufferr.pixelBuffer
if #available(iOS 15.0, *) {
DispatchQueue.global().async {
if let output = GreetingProcessor.shared.processVideoFrame(
foreground: pixelBufferRef,
background: self.vbImage) {
print("new output: \(output) => \(output.pixelBuffer) + \(self.buffer(from: output))")
guard let px = output.pixelBuffer else { return }
let rtcPixelBuffer = RTCCVPixelBuffer(pixelBuffer: px)
let i420buffer = rtcPixelBuffer.toI420()
let newFrame = RTCVideoFrame(buffer: i420buffer, rotation: frame.rotation, timeStampNs: frame.timeStampNs)
self.videoSource.capturer(capturer, didCapture: newFrame)
}
}
}
}
THen here is how I apply effect:
func blendImages(
background: CIImage,
foreground: CIImage,
mask: CIImage,
isRedMask: Bool = false
) -> CIImage? {
// scale mask
let maskScaleX = foreground.extent.width / mask.extent.width
let maskScaleY = foreground.extent.height / mask.extent.height
let maskScaled = mask.transformed(by: __CGAffineTransformMake(maskScaleX, 0, 0, maskScaleY, 0, 0))
// scale background
let backgroundScaleX = (foreground.extent.width / background.extent.width)
let backgroundScaleY = (foreground.extent.height / background.extent.height)
let backgroundScaled = background.transformed(
by: __CGAffineTransformMake(backgroundScaleX, 0, 0, backgroundScaleY, 0, 0))
let blendFilter = isRedMask ? CIFilter.blendWithRedMask() : CIFilter.blendWithMask()
blendFilter.inputImage = foreground
blendFilter.backgroundImage = backgroundScaled
blendFilter.maskImage = maskScaled
return blendFilter.outputImage
}
The problem is output.pixelBuffer always nil, so I can not create RTCFrame to pass it again to delegate
Can someone help?
UPDATE: This does not happen if I set only the board as the main view. Why would this happen when it is a subview?
I have a view that has a game board that you draw over to select characters. To do this I put a UIImageView over the game board and draw the line in touches moved using a helper function. It works, however, while drawing the line, everything that came before it slowly fades and also moves upwards.
It looks like this:
This is what the draw function looks like and it is called in touches moved:
//Helper function that draws a line which is used while the user is selecting letters
func drawLine(fromPoint: CGPoint, toPoint: CGPoint){
UIGraphicsBeginImageContext(drawingView.frame.size)
//get a context
guard let context: CGContext = UIGraphicsGetCurrentContext() else {
print("failed to get image context to draw line")
return
}
self.drawingView.image?.draw(in: CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height))
//set the line
context.move(to: fromPoint)
context.addLine(to: toPoint)
//customize line
context.setLineCap(CGLineCap.round)
context.setLineWidth(4)
context.setStrokeColor(UIColor.blue.cgColor)
context.setBlendMode(CGBlendMode.normal)
//draw the line
context.strokePath()
drawingView.image = UIGraphicsGetImageFromCurrentImageContext()
//close
UIGraphicsEndImageContext()
//print("Drew Line")
}
Heres all the code for the view:
/**
GameControllerDelegate is used to communicate when the user has chosen a word
*/
protocol GameControllerDelegate: AnyObject {
func wordPicked(letters: [String], moves: [(row: Int, column: Int)])
}
/**
GameViewDelegate is used to let the gameview know a user has picked a word using the board which is a subview
*/
protocol GameViewDelegate: AnyObject {
func wordPickedInBoard(letters: [String], moves: [(row: Int, column: Int)])
}
/**
Game view class holds all the subviews of the Game view as well as provides
getters and setters for each important data
*/
class GameView: UIView , GameViewDelegate{
//the board part
let boardView: BoardView = {
let boardView = BoardView()
boardView.backgroundColor = UIColor.white
boardView.translatesAutoresizingMaskIntoConstraints = true
return boardView
}()
//the score part
let scoreView: ScoreView = {
let scoreView = ScoreView()
scoreView.backgroundColor = UIColor.white
scoreView.translatesAutoresizingMaskIntoConstraints = true
return scoreView
}()
//two other vairables to assist in sizing the subviews
var minusTop: CGFloat = 0
var minusBottom: CGFloat = 0
//the board for the game
var board: [[String]] {
set
{
boardView.boardStrings = newValue
boardView.generateLabels()
setNeedsDisplay()
}
get { return boardView.boardStrings }
}
//the score
var score: Int {
set
{
scoreView.gameScore.text = " Score: " + String(newValue)
}
get
{
let indexStartOfNumber = scoreView.gameScore.text!.index((scoreView.gameScore.text!.startIndex), offsetBy: 9)
let numString = scoreView.gameScore.text?[indexStartOfNumber...]
return Int(String(describing: numString))!
}
}
//delegate for alerting the controller that a word was picked
var delegate: GameControllerDelegate? = nil
init(frame: CGRect, minusTop: CGFloat, minusBottom: CGFloat){
super.init(frame: frame)
boardView.delegate = self
self.minusTop = minusTop
self.minusBottom = minusBottom
addSubview(boardView)
addSubview(scoreView)
}
override init(frame: CGRect){
super.init(frame: frame)
boardView.delegate = self
addSubview(boardView)
addSubview(scoreView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("It's Apple. What did you expect?")
}
//manually layout the alarmpreview
override func layoutSubviews() {
var cursor: CGPoint = .zero
let scoreViewHeight = bounds.height/10
let boardHeight = bounds.height - minusTop - minusBottom - scoreViewHeight
cursor.y += minusTop
boardView.frame = CGRect(x: cursor.x, y: cursor.y, width: bounds.width, height: boardHeight)
cursor.y += boardHeight
scoreView.frame = CGRect(x: cursor.x, y: cursor.y, width: bounds.width, height: scoreViewHeight)
}
//function to catch when a word is picked in the board
func wordPickedInBoard(letters: [String], moves: [(row: Int, column: Int)]) {
//forward the info via the delegate
delegate?.wordPicked(letters: letters, moves: moves)
}
}
/**
Score view shows the users current score
*/
class ScoreView: UIView {
/*var scoreTitle: UILabel = {
let scoreTitle = UILabel()
scoreTitle.backgroundColor = UIColor.white
scoreTitle.text = "Score: "
return scoreTitle
}()*/
var gameScore: UILabel = {
let gameScore = UILabel()
gameScore.backgroundColor = UIColor.white
gameScore.text = " Score: 0"
return gameScore
}()
//init
override init(frame: CGRect){
super.init(frame: frame)
addSubview(gameScore)
//addSubview(scoreTitle)
}
required init?(coder aDecoder: NSCoder) {
fatalError("It's Apple. What did you expect?")
}
override func layoutSubviews() {
let cursor: CGPoint = .zero
gameScore.frame = CGRect(x: cursor.x, y: cursor.y, width: bounds.width, height: bounds.height)
}
}
/**
VoardView holds all the cells of the board and communicates when a user draws on it
*/
class BoardView: UIView {
var drawingView: UIImageView = {
let drawingView = UIImageView()
drawingView.backgroundColor = UIColor.white.withAlphaComponent(0.0)
return drawingView
}()
//delegate to communicate with the superview
var delegate: GameViewDelegate? = nil
//variables to aid in tracking the user moves
var lastPoint = CGPoint.zero
var looped = false
var boardStrings:[[String]] = []
var board:[[UILabel]] = Array(repeating: Array(repeating: UILabel(), count: 9), count: 12)
var curMoves: [(row: Int, column: Int)] = []
var highlightedArea: [(row: Int, column: Int)] = []
//centralized colors
let unHighlightedColor = UIColor.lightGray
let highlightedColor = UIColor.yellow
//Initialize this view
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.white
initBoard()
//self.boardStrings = generateBoard()
//generateLabels(boardStrings: self.boardStrings)
for i in 0...11 {
for j in 0...8 {
addSubview(board[i][j])
}
}
addSubview(drawingView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("It's Apple. What did you expect?")
}
//override the draw function, we draw the grid as well as the letters here
override func draw(_ rect: CGRect) {
super.draw(rect)
guard let context: CGContext = UIGraphicsGetCurrentContext() else {
print("failed to attain draw context")
return
}
//8 verticle lines
for i in 1 ... 9 {
let fromPoint = CGPoint(x: bounds.width * CGFloat(Double(i) * 1.0/9.0), y: 0)
let toPoint = CGPoint(x: bounds.width * CGFloat(Double(i) * 1.0/9.0), y: bounds.height)
context.move(to: fromPoint)
context.addLine(to: toPoint)
context.strokePath()
}
//draw 11 horizontal lines
for i in 1 ... 12 {
let fromPoint = CGPoint(x: 0, y: bounds.height * CGFloat(Double(i) * 1.0/12.0))
let toPoint = CGPoint(x: bounds.width, y: bounds.height * CGFloat(Double(i) * 1.0/12.0))
context.move(to: fromPoint)
context.addLine(to: toPoint)
context.strokePath()
}
}
//here we manually layout all the subviews that go into the preview
override func layoutSubviews() {
var cursor: CGPoint = .zero
let width = bounds.width/9
let height = bounds.height/12
for i in 0 ... 11 {
for j in 0 ... 8 {
board[i][j].frame = CGRect(x: cursor.x, y: cursor.y, width: width, height: height)
cursor.x += bounds.width * 1.0/9.0
}
cursor.x = 0
cursor.y += bounds.height * 1.0/12.0
}
drawingView.frame = self.bounds
}
//handle the beginning of a user move
override func touchesBegan(_ touches: Set<UITouch>, with: UIEvent?) {
looped = false
if let touch = touches.first as? UITouch {
lastPoint = touch.location(in: self)
//find what row we are in
let buttonHeight = bounds.height/12
let buttonWidth = bounds.width/9
let row: Int = Int(lastPoint.y/buttonHeight)
let column: Int = Int(lastPoint.x/buttonWidth)
curMoves.append((row: row, column: column))
highlightLabel(row: row, column: column)
setNeedsDisplay()
}
}
//handle when the user is moving. Calculate coordintes to drive the move the player is making
override func touchesMoved(_ touches: Set<UITouch>, with: UIEvent?) {
if !looped {
if let touch = touches.first as? UITouch {
let currentPoint = touch.location(in: self)
drawLine(fromPoint: lastPoint, toPoint: currentPoint)
//find what row we are in
let buttonHeight = bounds.height/12
let buttonWidth = bounds.width/9
let row: Int = Int(lastPoint.y/buttonHeight)
let column: Int = Int(lastPoint.x/buttonWidth)
if curMoves[curMoves.count - 1] != (row: row, column: column) {
//if the user is backstepping
if curMoves.count > 1 {
if curMoves[curMoves.count - 2] == (row: row, column: column) {
let pos = curMoves.remove(at: curMoves.count - 1)
unHeighlightLabel(row: pos.row, column: pos.column)
}
//if the user tried to make a loop
else if curMoves.contains(where: {$0 == (row: row, column: column)}){
for pos in curMoves {
unHeighlightLabel(row: pos.row, column: pos.column)
}
print("User tried to make a loop")
looped = true
}
else {
curMoves.append((row: row, column: column))
if board[row][column].backgroundColor != UIColor.green{
highlightLabel(row: row, column: column)
}
}
}
//if the user tried to make a loop
/*else if curMoves.contains(where: {$0 == (row: row, column: column)}){
for pos in curMoves {
unHeighlightLabel(row: pos.row, column: pos.column)
}
print("User tried to make a loop")
looped = true
}*/
else {
curMoves.append((row: row, column: column))
if board[row][column].backgroundColor != UIColor.green{
highlightLabel(row: row, column: column)
}
}
}
lastPoint = currentPoint
}
}
setNeedsDisplay()
}
//handle the end of a player move
override func touchesEnded(_ touches: Set<UITouch>, with: UIEvent?) {
if !looped {
//derive word
var word: [String] = []
for move in curMoves {
//if it contains blank put some garbage in there
if !boardStrings[move.row][move.column].contains("Blank") {
word.append(board[move.row][move.column].text!)
}
else {
word.append("###")
}
}
//let superview know
delegate?.wordPickedInBoard(letters: word, moves: curMoves)
}
//reset
for move in curMoves {
if board[move.row][move.column].backgroundColor != UIColor.green{
unHeighlightLabel(row: move.row, column: move.column)
}
}
curMoves = []
looped = false
drawingView.image = nil
setNeedsDisplay()
}
//Helper functions that changes the background color of the specified label to a centralized color
func highlightLabel(row: Int, column: Int){
board[row][column].backgroundColor = highlightedColor
}
func unHeighlightLabel(row: Int, column: Int) {
board[row][column].backgroundColor = unHighlightedColor
}
//Helper function that draws a line which is used while the user is selecting letters
func drawLine(fromPoint: CGPoint, toPoint: CGPoint){
UIGraphicsBeginImageContext(drawingView.frame.size)
//get a context
guard let context: CGContext = UIGraphicsGetCurrentContext() else {
print("failed to get image context to draw line")
return
}
self.drawingView.image?.draw(in: CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height))
//set the line
context.move(to: fromPoint)
context.addLine(to: toPoint)
//customize line
context.setLineCap(CGLineCap.round)
context.setLineWidth(4)
context.setStrokeColor(UIColor.blue.cgColor)
context.setBlendMode(CGBlendMode.normal)
//draw the line
context.strokePath()
drawingView.image = UIGraphicsGetImageFromCurrentImageContext()
//close
UIGraphicsEndImageContext()
//print("Drew Line")
}
//helper function that initilizes the board layout
func initBoard() {
for i in 0 ... 11 {
for j in 0 ... 8 {
board[i][j] = UILabel()
board[i][j].text = ""
}
}
}
/**
This function takes the generated board strings and converts them into their proper labels
*/
func generateLabels() {
highlightedArea = []
for i in 0 ... 11 {
for j in 0 ... 8 {
//board[i][j] = UILabel()
board[i][j].textAlignment = .center
board[i][j].layer.borderColor = UIColor.black.cgColor
board[i][j].layer.borderWidth = 1.0;
if boardStrings[i][j].hasSuffix("^") {//.characters.contains("^"){
//highlight the label
board[i][j].backgroundColor = UIColor.green
//set the text
let index = boardStrings[i][j].index(boardStrings[i][j].startIndex, offsetBy: boardStrings[i][j].count - 1)
let range = boardStrings[i][j].startIndex..<index
let letter = String(boardStrings[i][j][range])
board[i][j].backgroundColor = UIColor.green
board[i][j].text = letter
highlightedArea.append((row: i, column: j))
}
else if boardStrings[i][j] != "Blank" {
board[i][j].backgroundColor = unHighlightedColor
board[i][j].text = boardStrings[i][j]
}
else {
//leave it blank
board[i][j].backgroundColor = unHighlightedColor
board[i][j].text = ""
}
}
}
}
}
Recently experienced the same problem. It might be too late to help you out, but hopefully someone finds this useful someday.
Using floor() for height and width when setting the image view's bounds(image view you want to draw into) worked for me. Did this in objective C... don't know the floor() equivalent for swift.
As you can see above i trying to code an simple(!) subclass of UILabel to make an marquee or scrolling text effect if the text of the label is too long. I know that there are already good classes out there (e.g https://cocoapods.org/pods/MarqueeLabel), but i want to make my own :)
Down below you can see my current class.
I can't also fix an issue where the new label(s) are scrolling right, but there is also a third label which shouldn't be there. I think it's the label itself. But when i try the replace the first additional label with that label i won't work. I hope it's not too confusing :/
It's important to me that i only have to assign the class in the storyboard to the label. So that there is no need go and add code e.g in an view controller (beside the outlets). I hope it's clear what i want :D
So again:
Simple subclass of UILabel
scrolling label
should work without any additional code in other classes (except of outlets to change the labels text for example,...)
(it's my first own subclass, so feel free to teach me how to do it right :) )
Thank you very much !
It's by far not perfect, but this is my current class:
import UIKit
class LoopLabel: UILabel {
var labelText : String?
var rect0: CGRect!
var rect1: CGRect!
var labelArray = [UILabel]()
var isStop = false
var timeInterval: TimeInterval!
let leadingBuffer = CGFloat(25.0)
let loopStartDelay = 2.0
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.lineBreakMode = .byClipping
}
override var text: String? {
didSet {
labelText = text
setup()
}
}
func setup() {
let label = UILabel()
label.frame = CGRect.zero
label.text = labelText
timeInterval = TimeInterval((labelText?.characters.count)! / 5)
let sizeOfText = label.sizeThatFits(CGSize.zero)
let textIsTooLong = sizeOfText.width > frame.size.width ? true : false
rect0 = CGRect(x: leadingBuffer, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
rect1 = CGRect(x: rect0.origin.x + rect0.size.width, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
label.frame = rect0
super.clipsToBounds = true
labelArray.append(label)
self.addSubview(label)
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: 0, height: 0))
if textIsTooLong {
let additionalLabel = UILabel(frame: rect1)
additionalLabel.text = labelText
self.addSubview(additionalLabel)
labelArray.append(additionalLabel)
animateLabelText()
}
}
func animateLabelText() {
if(!isStop) {
let labelAtIndex0 = labelArray[0]
let labelAtIndex1 = labelArray[1]
UIView.animate(withDuration: timeInterval, delay: loopStartDelay, options: [.curveLinear], animations: {
labelAtIndex0.frame = CGRect(x: -self.rect0.size.width,y: 0,width: self.rect0.size.width,height: self.rect0.size.height)
labelAtIndex1.frame = CGRect(x: labelAtIndex0.frame.origin.x + labelAtIndex0.frame.size.width,y: 0,width: labelAtIndex1.frame.size.width,height: labelAtIndex1.frame.size.height)
}, completion: { finishied in
labelAtIndex0.frame = self.rect1
labelAtIndex1.frame = self.rect0
self.labelArray[0] = labelAtIndex1
self.labelArray[1] = labelAtIndex0
self.animateLabelText()
})
} else {
self.layer.removeAllAnimations()
}
}
}
First of, I would keep the variables private if you don't need them to be accessed externally, especially labelText (since you're using the computed property text to be set).
Second, since you're adding labels as subviews, I'd rather use a UIView as container instead of UILabel. The only difference in the storyboard would be to add a View instead of a Label.
Third, if you use this approach, you should not set the frame (of the view) to zero.
Something like that would do:
import UIKit
class LoopLabelView: UIView {
private var labelText : String?
private var rect0: CGRect!
private var rect1: CGRect!
private var labelArray = [UILabel]()
private var isStop = false
private var timeInterval: TimeInterval!
private let leadingBuffer = CGFloat(25.0)
private let loopStartDelay = 2.0
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var text: String? {
didSet {
labelText = text
setup()
}
}
func setup() {
self.backgroundColor = UIColor.yellow
let label = UILabel()
label.text = labelText
label.frame = CGRect.zero
timeInterval = TimeInterval((labelText?.characters.count)! / 5)
let sizeOfText = label.sizeThatFits(CGSize.zero)
let textIsTooLong = sizeOfText.width > frame.size.width ? true : false
rect0 = CGRect(x: leadingBuffer, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
rect1 = CGRect(x: rect0.origin.x + rect0.size.width, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
label.frame = rect0
super.clipsToBounds = true
labelArray.append(label)
self.addSubview(label)
//self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: 0, height: 0))
if textIsTooLong {
let additionalLabel = UILabel(frame: rect1)
additionalLabel.text = labelText
self.addSubview(additionalLabel)
labelArray.append(additionalLabel)
animateLabelText()
}
}
func animateLabelText() {
if(!isStop) {
let labelAtIndex0 = labelArray[0]
let labelAtIndex1 = labelArray[1]
UIView.animate(withDuration: timeInterval, delay: loopStartDelay, options: [.curveLinear], animations: {
labelAtIndex0.frame = CGRect(x: -self.rect0.size.width,y: 0,width: self.rect0.size.width,height: self.rect0.size.height)
labelAtIndex1.frame = CGRect(x: labelAtIndex0.frame.origin.x + labelAtIndex0.frame.size.width,y: 0,width: labelAtIndex1.frame.size.width,height: labelAtIndex1.frame.size.height)
}, completion: { finishied in
labelAtIndex0.frame = self.rect1
labelAtIndex1.frame = self.rect0
self.labelArray[0] = labelAtIndex1
self.labelArray[1] = labelAtIndex0
self.animateLabelText()
})
} else {
self.layer.removeAllAnimations()
}
}
}
I'm working with google maps sdk in ios9 and I'm currently displaying tile layers over the map with different colors that represent a value. I want to add the ability to tap a tile and display information about the tile such as what the actual value of that tile is. I looked online and the only thing I found was to make transparent markers for each tile and then write a handler for each marker to display the value, but I think there has to be a more efficient way to do this. Can someone lead me in the right direction? Thanks!
class MyMapView: UIView{
var myMap: GMSMapView!
var closestPoint: CLLocationCoordinate2D!
required init?(coder aDecoder: NSCoder) {
fatalError("This class does not support NSCoding")
}
init(closestPoint: CLLocationCoordinate2D!){
super.init(frame: CGRectZero)
self.backgroundColor = UIVariables.blackOpaque
self.closestPoint = closestPoint
var camera = GMSCameraPosition.cameraWithLatitude(AppVariables.location2D!.latitude, longitude: AppVariables.location2D!.longitude, zoom: 6)
self.myMap = GMSMapView.mapWithFrame(CGRectZero, camera: camera)
self.myMap.mapType = GoogleMaps.kGMSTypeSatellite
delay(seconds: 0.5) { () -> () in
let path = GMSMutablePath()
path.addCoordinate(self.closestPoint!)
path.addCoordinate(AppVariables.location2D!)
let bounds = GMSCoordinateBounds(path: path)
self.myMap.moveCamera(GMSCameraUpdate.fitBounds(bounds, withPadding: 50.0))
let layer = MyMapTileLayer()
layer.map = self.myMap
}
self.myMap.translatesAutoresizingMaskIntoConstraints = false
//self.frame = CGRectMake(0, 0, UIScreen.mainScreen().bounds.width, 400)
self.addSubview(self.myMap)
let viewsDictionary = ["map": self.myMap]
let metricDictionary = ["width": self.frame.size.width/2]
let view_constraint_V1:Array = NSLayoutConstraint.constraintsWithVisualFormat("V:|[map]-15-|", options: NSLayoutFormatOptions.AlignAllLeading, metrics: metricDictionary, views: viewsDictionary)
let view_constraint_H1:Array = NSLayoutConstraint.constraintsWithVisualFormat("H:|-10-[map]-10-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: metricDictionary, views: viewsDictionary)
self.addConstraints(view_constraint_V1)
self.addConstraints(view_constraint_H1)
}
func delay(seconds seconds: Double, completion:()->()) {
let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64( Double(NSEC_PER_SEC) * seconds ))
dispatch_after(popTime, dispatch_get_main_queue()) {
completion()
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
class MyTileLayer: GMSTileLayer{
var webService = WebService()
override func requestTileForX(x: UInt, y: UInt, zoom: UInt, receiver: GMSTileReceiver!) {
webService.getTiles(x, y: y, z: zoom, completion: { (result) -> Void in
receiver.receiveTileWithX(x, y: y, zoom: zoom, image: UIImage(data: result))
})
}
}
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
}