I have a situation, which I do not understand.
I like to create CATextLayers in a background task and show them in my view, because it takes some time.
This works perfect without a background task. I can see the text immediately in my view "worldmapview"
#IBOutlet var worldmapview: Worldmapview! // This view is just an empty view.
override func viewDidLoad(){
view.addSubview(worldmapview);
}
func addlayerandrefresh_direct(){
var calayer=CALayer();
var newtextlayer:CATextLayer=create_text_layer(0,y1: 0,width:1000,heigth:1000,text:"My Text ....",fontsize:5);
self.worldmapview.layer.addSublayer(newtextlayer)
//calayer.addSublayer(newtextlayer);
calayer.addSublayer(newtextlayer)
self.worldmapview.layer.addSublayer(calayer);
self.worldmapview.setNeedsDisplay();
}
When doing this in a backgroundtask, the text does not appear in my view. Sometimes, not always, it appears after some seconds (10 for example).
func addlayerandrefresh_background(){
let qualityOfServiceClass = QOS_CLASS_BACKGROUND
let backgroundQueue = dispatch_get_global_queue(qualityOfServiceClass, 0)
dispatch_async(backgroundQueue, {
var calayer=CALayer();
var newtextlayer:CATextLayer=create_text_layer(0,y1: 0,width:1000,heigth:1000,text:"My Text ....",fontsize:5);
dispatch_async(dispatch_get_main_queue(),{
self.worldmapview.layer.addSublayer(newtextlayer)
//calayer.addSublayer(newtextlayer);
calayer.addSublayer(newtextlayer)
self.worldmapview.layer.addSublayer(calayer);
self.worldmapview.setNeedsDisplay();
})
})
}
func create_text_layer(x1:CGFloat,y1:CGFloat,width:CGFloat,heigth:CGFloat,text:String,fontsize:CGFloat) -> CATextLayer {
let textLayer = CATextLayer()
textLayer.frame = CGRectMake(x1, y1, width, heigth);
textLayer.string = text
let fontName: CFStringRef = "ArialMT"
textLayer.font = CTFontCreateWithName(fontName, fontsize, nil)
textLayer.fontSize=fontsize;
textLayer.foregroundColor = UIColor.darkGrayColor().CGColor
textLayer.wrapped = true
textLayer.alignmentMode = kCAAlignmentLeft
textLayer.contentsScale = UIScreen.mainScreen().scale
return textLayer;
}
Does someone see, what is wrong ?
What is very confusing : The same doing with CAShapeLayer works in the background.
Looks like setNeedDisplay not cause sublayers redraw and we need call it on all layers we need, in this case it's newly added layer
func addlayerandrefresh_background(){
let calayer = CALayer()
calayer.frame = self.worldmapview.bounds
dispatch_async(backgroundQueue, {
for var i = 0; i < 100 ; i+=10 {
let newtextlayer:CATextLayer=self.create_text_layer(0, y1: CGFloat(i) ,width:200,heigth:200,text:"My Text ....",fontsize:5)
calayer.addSublayer(newtextlayer)
}
dispatch_async(dispatch_get_main_queue(),{
self.worldmapview.layer.addSublayer(calayer)
for l in calayer.sublayers! {
l.setNeedsDisplay()
}
})
})
}
All the changes on UI are performed on main thread. You cannot update User Interface on background thread. According to Apple's documentation
Work involving views, Core Animation, and many other UIKit classes
usually must occur on the app’s main thread. There are some exceptions
to this rule—for example, image-based manipulations can often occur on
background threads—but when in doubt, assume that work needs to happen
on the main thread.
Related
In the following code, I want to draw a progress bar that grows. what should I call at the line marked
// what goes here?
Assume the code is run on the main thread.
class View: UIView {
func updateProgressBar(){
var boxFrame = CGRect(x:10, y:10: width:100, height: 100)
for _ in 0...<10 {
let box = UIView(frame: boxFrame)
box.backgroundColor = .blue
addSubview(box)
// What goes here?
Thread.sleep(forTimeInterval: 2)
boxFrame.origin.x = boxFrame.width
}
}
}
What will be the answer from the following option and why?
setNeedsDisplay()
layer.draw(in: UIGraphicsGetCurrentConntext()!)
draw(bounds)
This approach is incorrect. It should be implemented in another way. such as with DispatchQueue.asyncAfter()
I have to render create a huge amount(~10000) of UIImage objects, which are later added to a map using MapBox. The image are created by rendering a UILabel to an UIImage (sadly, the label can not directly be rendered on the map in the right font).
I imagined that, because I am not adding the views to the hierarchy until they are rendered on the map, I could create the views on a background thread in order to not have the UI freeze. However, this seems not to be possible.
My question is, is there a way to create a huge amount of UIImage objects by rendering a UILabel to an image without freezing the UI? Thanks for any help!
My code to render a UIView to an image is as follows:
private func render(_ view: UIView) -> UIImage? {
UIGraphicsBeginImageContext(view.frame.size)
guard let currentContext = UIGraphicsGetCurrentContext() else {return nil}
view.layer.render(in: currentContext)
return UIGraphicsGetImageFromCurrentImageContext()
}
And an example of a label I render to an image:
bigItemList.forEach { item in
// work to process the item..
// I have to call the rendering om the main thread, which causes the UI to freeze
DispatchQueue.main.async {
addImage(createLabel(text: label))
}
}
func createLabel(text: String?) -> UIImage? {
let label = UILabel()
label.textAlignment = .center
label.font = .boldSystemFont(ofSize: 14)
label.textColor = .mainBlue
label.text = text
label.sizeToFit()
label render(depthLabel)
}
UILabel is not safe to access on any non-main thread. The tool you want here is CATextLayer which will do all the same things (with a slightly clunkier syntax) while being thread-safe. For example:
func createLabel(text: String) -> UIImage? {
let label = CATextLayer()
let uiFont = UIFont.boldSystemFont(ofSize: 14)
label.font = CGFont(uiFont.fontName as CFString)
label.fontSize = 14
label.foregroundColor = UIColor.blue.cgColor
label.string = text
label.bounds = CGRect(origin: .zero, size: label.preferredFrameSize())
let renderer = UIGraphicsImageRenderer(size: label.bounds.size)
return renderer.image { context in
label.render(in: context.cgContext)
}
}
This is safe to run on any queue.
You most probably are already in main queue. Try execuring the loop in another one:
DispatchQueue.init(label: "Other queue").async {
bigItemList.forEach { item in
// work to process the item..
// I have to call the rendering om the main thread, which causes the UI to freeze
DispatchQueue.main.async {
addImage(createLabel(text: label))
}
}
}
EDIT: If it is already in another queue, you are also missing the UIGraphicsEndImageContext() call into render:
private func render(_ view: UIView) -> UIImage? {
UIGraphicsBeginImageContext(view.frame.size)
guard let currentContext = UIGraphicsGetCurrentContext() else {return nil}
view.layer.render(in: currentContext)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
EDIT: Another thing I see is that label allocation is the most time consuming in your loop. Try allocating it once since it's reusable:
let label = UILabel()
func createLabel(text: String?) -> UIImage? {
label.textAlignment = .center
...
...
}
OK. There seems to be a dearth of examples on this, and I am fairly stumped.
I'm trying to make a custom print page renderer; the type that completely customizes the output, not one that uses an existing view.
The really weird thing, is that I was able to do this in ObjC a couple of years ago, and I can't seem to do the same thing in Swift.
I should mention that I am using the prerelease (Beta 5) of Xcode, and Swift 4 (Which has almost no difference at all from Swift 3, in my project).
The project is here.
It's a completely open-source project, so nothing's hidden; however, it's still very much under development, and is a moving target.
This is the page renderer class.
BTW: Ignore the delegate class. I was just thrashing around, trying to figure stuff up. I'm not [yet] planning on doing any delegate stuff.
In particular, my question concerns what's happening here:
override func drawContentForPage(at pageIndex: Int, in contentRect: CGRect) {
let perMeetingHeight: CGFloat = self.printableRect.size.height / CGFloat(self.actualNumberOfMeetingsPerPage)
let startingPoint = max(self.maxMeetingsPerPage * pageIndex, 0)
let endingPointPlusOne = min(self.maxMeetingsPerPage, self.actualNumberOfMeetingsPerPage)
for index in startingPoint..<endingPointPlusOne {
let top = self.printableRect.origin.y + (CGFloat(index) * perMeetingHeight)
let meetingRect = CGRect(x: self.printableRect.origin.x, y: top, width: self.printableRect.size.width, height: perMeetingHeight)
self.drawMeeting(at: index, in: meetingRect)
}
}
and here:
func drawMeeting(at meetingIndex: Int, in contentRect: CGRect) {
let myMeetingObject = self.meetings[meetingIndex]
var top: CGFloat = contentRect.origin.y
let topLabelRect = CGRect(x: contentRect.origin.x, y: 0, width: contentRect.size.width, height: self.meetingNameHeight)
top += self.meetingNameHeight
let meetingNameLabel = UILabel(frame: topLabelRect)
meetingNameLabel.backgroundColor = UIColor.clear
meetingNameLabel.font = UIFont.boldSystemFont(ofSize: 30)
meetingNameLabel.textAlignment = .center
meetingNameLabel.textColor = UIColor.black
meetingNameLabel.text = myMeetingObject.name
meetingNameLabel.draw(topLabelRect)
}
Which is all called from here:
#IBAction override func actionButtonHit(_ sender: Any) {
let sharedPrintController = UIPrintInteractionController.shared
let printInfo = UIPrintInfo(dictionary:nil)
printInfo.outputType = UIPrintInfoOutputType.general
printInfo.jobName = "print Job"
sharedPrintController.printPageRenderer = BMLT_MeetingSearch_PageRenderer(meetings: self.searchResults)
sharedPrintController.present(from: self.view.frame, in: self.view, animated: false, completionHandler: nil)
}
What's going on, is that everything on a page is being piled at the top. I am trying to print a sequential list of meetings down a page, but they are all getting drawn at the y=0 spot, like so:
This should be a list of meeting names, running down the page.
The way to get here, is to start the app, wait until it's done connecting to the server, then bang the big button. You'll get a list, and press the "Action" button at the top of the screen.
I haven't bothered to go beyond the preview, as that isn't even working. The list is the only one I have wired up right now, and I'm just at the stage of simply printing the meeting names to make sure I have the basic layout right.
Which I obviously don't.
Any ideas?
All right. I figured out what the issue was.
I was trying to do this using UIKit routines, which assume a fairly high-level drawing context. The drawText(in: CGRect) thing was my lightbulb.
I need to do everything using lower-level, context-based drawing, and leave UIKit out of it.
Here's how I implement the drawMeeting routine now (I have changed what I draw to display more relevant information). I'm still working on it, and it will get larger:
func drawMeeting(at meetingIndex: Int, in contentRect: CGRect) {
let myMeetingObject = self.meetings[meetingIndex]
var remainingRect = contentRect
if (1 < self.meetings.count) && (0 == meetingIndex % 2) {
if let drawingContext = UIGraphicsGetCurrentContext() {
drawingContext.setFillColor(UIColor.black.withAlphaComponent(0.075).cgColor)
drawingContext.fill(contentRect)
}
}
var attributes: [NSAttributedStringKey : Any] = [:]
attributes[NSAttributedStringKey.font] = UIFont.italicSystemFont(ofSize: 12)
attributes[NSAttributedStringKey.backgroundColor] = UIColor.clear
attributes[NSAttributedStringKey.foregroundColor] = UIColor.black
let descriptionString = NSAttributedString(string: myMeetingObject.description, attributes: attributes)
let descriptionSize = contentRect.size
var stringRect = descriptionString.boundingRect(with: descriptionSize, options: [NSStringDrawingOptions.usesLineFragmentOrigin,NSStringDrawingOptions.usesFontLeading], context: nil)
stringRect.origin = contentRect.origin
descriptionString.draw(at: stringRect.origin)
remainingRect.origin.y -= stringRect.size.height
}
I have the following code where I create a sprite node that displays an animated GIF. I want to create another function that darkens the GIF when called upon. I could still be able to watch the animation, but the content would be visibly darker. I'm not sure how to approach this. Should I individually darken every texture or frame used to create the animation? If so, how do I darken a texture or frame in the first place?
// Extract frames and duration
guard let imageData = try? Data(contentsOf: url as URL) else {
return
}
let source = CGImageSourceCreateWithData(imageData as CFData, nil)
var images = [CGImage]()
let count = CGImageSourceGetCount(source!)
var delays = [Int]()
// Fill arrays
for i in 0..<count {
// Add image
if let image = CGImageSourceCreateImageAtIndex(source!, i, nil) {
images.append(image)
}
// At it's delay in cs
let delaySeconds = UIImage.delayForImageAtIndex(Int(i),
source: source)
delays.append(Int(delaySeconds * 1000.0)) // Seconds to ms
}
// Calculate full duration
let duration: Int = {
var sum = 0
for val: Int in delays {
sum += val
}
return sum
}()
// Get frames
let gcd = SKScene.gcdForArray(delays)
var frames = [SKTexture]()
var frame: SKTexture
var frameCount: Int
for i in 0..<count {
frame = SKTexture(cgImage: images[Int(i)])
frameCount = Int(delays[Int(i)] / gcd)
for _ in 0..<frameCount {
frames.append(frame)
}
}
let gifNode = SKSpriteNode.init(texture: frames[0])
gifNode.position = CGPoint(x: skScene.size.width / 2.0, y: skScene.size.height / 2.0)
gifNode.name = "content"
// Add animation
let gifAnimation = SKAction.animate(with: frames, timePerFrame: ((Double(duration) / 1000.0)) / Double(frames.count))
gifNode.run(SKAction.repeatForever(gifAnimation))
skScene.addChild(gifNode)
I would recommend to use the colorize(with:colorBlendFactor:duration:) method. It is an SKAction that animates changing the color of a whole node. That way you don't have to get into darkening the individual textures or frames, and it also adds a nice transition from a non-dark to a darkened color. Once the action ends, the node will stay darkened until you undarken it, so any changes to the node's texture will also be visible as darkend to the user.
Choose whatever color and colorBlendFactor will work best for you to have the darkened effect you need, e.g. you could set the color to .black and colorBlendFactor to 0.3. To undarken, just set the color to .clear and colorBlendFactor to 0.
Documentation here.
Hope this helps!
I have a drawing app that uses CALayer sublayers for the actual drawing of an image. I currently have an IBAction that contains code to remove the sublayers from the superlayer. However everytime I run it, I get a BAD ACCESS error that crashes my app. I'm wondering why it won't allow me to remove the sublayers. Also, in theory, this approach would remove all of the layers entirely. I would ideally want it to only undo the last layers that were drawn. Any suggestions on what I should do? Thanks.
var locale: CALayer {
return layerView.layer
}
#IBAction func undoButton(sender: AnyObject) {
var sublayers = self.view.layer.sublayers
for layer in sublayers {
layer.removeFromSuperlayer()
}
}
func setUpLayer() -> CALayer {
locale.contents = image
locale.contentsGravity = kCAGravityCenter
return locale
}
func subLayerDisplay() {
var newLayer = CALayer()
var tempRect = CGRectMake(prevLocation.x, prevLocation.y, 50, 50)
newLayer.frame = tempRect
newLayer.contents = image
self.view.layer.insertSublayer(newLayer, below: locale)
}
To sum up, you can add your layer to a view, and then deal with the view, which will make life much more easier.
When you add the button, you can do
view.tag = 1
self.addSubView(view)
When you want to remove it
let view = self.viewWithTag(1) as! UIView
view.removeFromSuperView()