why doesn't my UIImage appear in my tabBar? - swift

I'm trying to add an UIImage to customise my tabBar just like I did with my navigationBar, but it seems not work.
First I create a CAGradientLayer and then I turn it into UIImage. Here is the code:
extension CALayer {
func createImageFromLayer() -> UIImage?{
frame = CGRect(x: 0, y: 0, width: 1000, height: 1000)
UIGraphicsBeginImageContext(frame.size)
guard let context = UIGraphicsGetCurrentContext() else {return nil}
render(in: context)
let output = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return output
}
}
class CustomTabBarController: UITabBarController {
let grad: CAGradientLayer = {
let gradient = CAGradientLayer()
gradient.colors = [UIColor.clear, UIColor.black]
gradient.locations = [0,1]
return gradient
}()
override func viewDidLoad() {
super.viewDidLoad()
let tabImage = grad.createImageFromLayer()
self.tabBar.isTranslucent = true
self.tabBar.backgroundImage = tabImage
self.tabBar.shadowImage = UIImage()
}
}
Setting an image for my navigationBar worked perfectly using this method:
navigationController?.navigationBar.setBackgroundImage(barImg, for: UIBarMetrics.default)
In this code barImg is UIImage created from a CAGradientLayer just like with tabBar. I've checked if tabImage is nil and it's not.
It seems like tabBar doesn't have a function like tabBar.setBackgroundImage like navigationBar. How do I go about doing this?
The only result I get is a completely clear background for my tabBar with no gradient added and I don't know what I'm missing here once tabBar.backgroundImage doesn't work.
Thank you for the answers.

At first, when you setup colors for a CAGradientLayer you should use CGColor objects only. So you need to change that line to
gradient.colors = [UIColor.clear.cgColor, UIColor.black.cgColor]
After that your tabImage won't be empty and become visible.
The last problem is that you always set your image size to 1000x1000 pixels. That's way too much. To fix that you can pass target frame size to createImageFromLayer as a parameter. Its signature will become something like this.
func createImageFromLayer(targetFrame: CGRect) -> UIImage?
In your case targetFrame is tabBar.bounds.

Related

CAGradientLayer not fit perfectly on UITableViewCell bounds

i am developing simple UITableView with custom UITableViewCell with Xib file
i have added MainView and pin it to ContentView in order to add all my views inside MainView
as shown below
UITableViewCell layout
when i try to add CAGradientLayer to MainView it doesnt fit perfectly on mainView bounds as i show below
whats happening on simulator
i use this piece of code to add CAGradientLayer
override func awakeFromNib() {
super.awakeFromNib()
let gradient = CAGradientLayer()
mainView.layer.cornerRadius = 10
gradient.frame = mainView.frame
gradient.colors = [ColorCompatibility.gradEnd.cgColor, ColorCompatibility.gradStart.cgColor]
gradient.endPoint = CGPoint(x: 0.5, y: 1)
gradient.startPoint = CGPoint(x: 0.5, y: 0)
// gradient.cornerRadius = 20
gradient.shadowColor = ColorCompatibility.systemBackground.cgColor
gradient.shadowOpacity = 0.8
gradient.shadowOffset = .zero
gradient.shadowRadius = 5
gradient.shadowPath = UIBezierPath(roundedRect: mainView.layer.bounds, cornerRadius: 20).cgPath
gradient.shouldRasterize = true
gradient.rasterizationScale = UIScreen.main.scale
contentView.layer.insertSublayer(gradient, at: 0)
}
how can i fix this?
Two issues here. The first is that in awakeFromNib the cell (and its content view) most likely doesn't have the correct frame so you could use another life cycle hook where the frame is correct but then you would run into problems when the device is rotated so the cell frame changes at runtime.
I would suggest to update the gradient layers frame in layoutSublayers(of:). Let's assume you created a property for the layer called gradient:
override func layoutSublayers(of layer: CALayer) {
super.layoutSublayers(of: layer)
gradient.frame = mainView.bounds
}

How do I position an image correctly in MTKView?

I am trying to implement an image editing view using MTKView and Core Image filters and have the basics working and can see the filter applied in realtime. However the image is not positioned correctly in the view - can someone point me in the right direction for what needs to be done to get the image to render correctly in the view. It needs to fit the view and retain its original aspect ratio.
Here is the metal draw function - and the empty drawableSizeWillChange!? - go figure. its probably also worth mentioning that the MTKView is a subview of another view in a ScrollView and can be resized by the user. It's not clear to me how Metals handles resizing the view but it seems that doesn't come for free.
I am also trying to call the draw() function from a background thread and this appears to sort of work. I can see the filter effects as they are applied to the image using a slider. As I understand it this should be possible.
It also seems that the coordinate space for rendering is in the images coordinate space - so if the image is smaller than the MTKView then to position the image in the centre the X and Y coordinates will be negative.
When the view is resized then everything gets crazy with the image suddenly becoming way too big and parts of the background not being cleared.
Also when resting the view the image gets stretched rather than redrawing smoothly.
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
}
public func draw(in view: MTKView) {
if let ciImage = self.ciImage {
if let currentDrawable = view.currentDrawable { // 1
let commandBuffer = commandQueue.makeCommandBuffer()
let inputImage = ciImage // 2
exposureFilter.setValue(inputImage, forKey: kCIInputImageKey)
exposureFilter.setValue(ev, forKey: kCIInputEVKey)
context.render(exposureFilter.outputImage!,
to: currentDrawable.texture,
commandBuffer: commandBuffer,
bounds: CGRect(origin: .zero, size: view.drawableSize),
colorSpace: colorSpace)
commandBuffer?.present(currentDrawable)
commandBuffer?.commit()
}
}
}
As you can see the image is on the bottom left
let scaleFilter = CIFilter(name: "CILanczosScaleTransform")
That should help you out. The issue is that your CIImage, wherever it might come from, is not the same size as the view you are rendering it in.
So what you could opt to do is calculate the scale, and apply it as a filter:
let scaleFilter = CIFilter(name: "CILanczosScaleTransform")
scaleFilter?.setValue(ciImage, forKey: kCIInputImageKey)
scaleFilter?.setValue(scale, forKey: kCIInputScaleKey)
This resolves your scale issue; I currently do not know what the most efficient approach would be to actually reposition the image
Further reference: https://nshipster.com/image-resizing/
The problem is your call to context.render — you are calling render with bounds: origin .zero. That’s the lower left.
Placing the drawing in the correct spot is up to you. You need to work out where the right bounds origin should be, based on the image dimensions and your drawable size, and render there. If the size is wrong, you also need to apply a scale transform first.
Thanks to Tristan Hume's MetalTest2 I now have it working nicely in two synchronised scrollViews. The basics are in the subclass below - the renderer and shaders can be found at Tristan's MetalTest2 project. This class is managed by a viewController and is a subview of the scrollView's documentView. See image of the final result.
//
// MetalLayerView.swift
// MetalTest2
//
// Created by Tristan Hume on 2019-06-19.
// Copyright © 2019 Tristan Hume. All rights reserved.
//
import Cocoa
// Thanks to https://stackoverflow.com/questions/45375548/resizing-mtkview-scales-old-content-before-redraw
// for the recipe behind this, although I had to add presentsWithTransaction and the wait to make it glitch-free
class ImageMetalView: NSView, CALayerDelegate {
var renderer : Renderer
var metalLayer : CAMetalLayer!
var commandQueue: MTLCommandQueue!
var sourceTexture: MTLTexture!
let colorSpace = CGColorSpaceCreateDeviceRGB()
var context: CIContext!
var ciMgr: CIManager?
var showEdits: Bool = false
var ciImage: CIImage? {
didSet {
self.metalLayer.setNeedsDisplay()
}
}
#objc dynamic var fileUrl: URL? {
didSet {
if let url = fileUrl {
self.ciImage = CIImage(contentsOf: url)
}
}
}
/// Bind to this property from the viewController to receive notifications of changes to CI filter parameters
#objc dynamic var adjustmentsChanged: Bool = false {
didSet {
self.metalLayer.setNeedsDisplay()
}
}
override init(frame: NSRect) {
let _device = MTLCreateSystemDefaultDevice()!
renderer = Renderer(pixelFormat: .bgra8Unorm, device: _device)
self.commandQueue = _device.makeCommandQueue()
self.context = CIContext()
self.ciMgr = CIManager(context: self.context)
super.init(frame: frame)
self.wantsLayer = true
self.layerContentsRedrawPolicy = .duringViewResize
// This property only matters in the case of a rendering glitch, which shouldn't happen anymore
// The .topLeft version makes glitches less noticeable for normal UIs,
// while .scaleAxesIndependently matches what MTKView does and makes them very noticeable
// self.layerContentsPlacement = .topLeft
self.layerContentsPlacement = .scaleAxesIndependently
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func makeBackingLayer() -> CALayer {
metalLayer = CAMetalLayer()
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.device = renderer.device
metalLayer.delegate = self
// If you're using the strategy of .topLeft placement and not presenting with transaction
// to just make the glitches less visible instead of eliminating them, it can help to make
// the background color the same as the background of your app, so the glitch artifacts
// (solid color bands at the edge of the window) are less visible.
// metalLayer.backgroundColor = CGColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
metalLayer.allowsNextDrawableTimeout = false
// these properties are crucial to resizing working
metalLayer.autoresizingMask = CAAutoresizingMask(arrayLiteral: [.layerHeightSizable, .layerWidthSizable])
metalLayer.needsDisplayOnBoundsChange = true
metalLayer.presentsWithTransaction = true
metalLayer.framebufferOnly = false
return metalLayer
}
override func setFrameSize(_ newSize: NSSize) {
super.setFrameSize(newSize)
self.size = newSize
renderer.viewportSize.x = UInt32(newSize.width)
renderer.viewportSize.y = UInt32(newSize.height)
// the conversion below is necessary for high DPI drawing
metalLayer.drawableSize = convertToBacking(newSize)
self.viewDidChangeBackingProperties()
}
var size: CGSize = .zero
// This will hopefully be called if the window moves between monitors of
// different DPIs but I haven't tested this part
override func viewDidChangeBackingProperties() {
guard let window = self.window else { return }
// This is necessary to render correctly on retina displays with the topLeft placement policy
metalLayer.contentsScale = window.backingScaleFactor
}
func display(_ layer: CALayer) {
if let drawable = metalLayer.nextDrawable(),
let commandBuffer = commandQueue.makeCommandBuffer() {
let passDescriptor = MTLRenderPassDescriptor()
let colorAttachment = passDescriptor.colorAttachments[0]!
colorAttachment.texture = drawable.texture
colorAttachment.loadAction = .clear
colorAttachment.storeAction = .store
colorAttachment.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
if let outputImage = self.ciImage {
let xscale = self.size.width / outputImage.extent.width
let yscale = self.size.height / outputImage.extent.height
let scale = min(xscale, yscale)
if let scaledImage = self.ciMgr!.scaleTransformFilter(outputImage, scale: scale, aspectRatio: 1),
let processed = self.showEdits ? self.ciMgr!.processImage(inputImage: scaledImage) : scaledImage {
let x = self.size.width/2 - processed.extent.width/2
let y = self.size.height/2 - processed.extent.height/2
context.render(processed,
to: drawable.texture,
commandBuffer: commandBuffer,
bounds: CGRect(x:-x, y:-y, width: self.size.width, height: self.size.height),
colorSpace: colorSpace)
}
} else {
print("Image is nil")
}
commandBuffer.commit()
commandBuffer.waitUntilScheduled()
drawable.present()
}
}
}

Take screenshot of visible area of UIScrollView

I have a UIScrollView as subview of a view controller main view. I'm trying to save a screenshot of whatever is visible in the frame of the UIScrollView only.
UIGraphicsBeginImageContextWithOptions(imageScrollView.bounds.size, false, UIScreen.main.scale)
let context = UIGraphicsGetCurrentContext()
context?.translateBy(x: 0, y: 0)
view.layer.render(in: context!)
let visibleScrollViewImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let popupImageView = UIImageView(image: visibleScrollViewImage)
popupImageView.layer.borderColor = UIColor.white.cgColor
popupImageView.layer.borderWidth = 4
popupImageView.frame = CGRect(origin: CGPoint(x: 0, y: 400), size: CGSize(width: 400, height: 400))
imageScrollView.removeFromSuperview()
view.addSubview(popupImageView)
The part with the popupImageView is just to test out and see what is actually saving, it seems there is some offset problem, the horizontal axis is fine but i seem to be getting just the top third of the image I want, and above that is just dark space.
Seems like it must be a pretty easy solution but I've searched through all similar questions and can't find an answers.
Thanks heaps!
Try the following. If I do it with a UITableView control, it works.
import UIKit
class ViewController: UIViewController {
#IBAction func snapTapped(_ sender: UIButton) {
UIGraphicsBeginImageContextWithOptions(imageScrollView.bounds.size, true, 1.0)
imageScrollView.drawHierarchy(in: CGRect(origin: CGPoint.zero, size: imageScrollView.bounds.size), afterScreenUpdates: true)
if let image = UIGraphicsGetImageFromCurrentImageContext() {
UIGraphicsEndImageContext()
myImageView.image = image
}
}
}
func screenShotMethod()->UIImage
{
let layer = self.imageScrollView.layer
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, scale);
layer.render(in: UIGraphicsGetCurrentContext()!)
let screenshot = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return screenshot!
}

How to get rid of border of UISearchBar?

The Problem
I have a UISearchController embedded within a UIView. The searchbar takes up the entire size of the view. However, there's this grey border that I can't figure how to get rid off. I want the search bar to take up the entire size of the view without that pesky grey border. How can I get rid of it?
I've tried
searchController?.searchBar.backgroundImage = UIImage()
But all that does is make the grey border clear (not to mention it makes the searchResultsController of the searchController not show up):
I want the border to be gone and the search bar take up the entire frame of the UIView.
let SEARCH_BAR_SEARCH_FIELD_KEY = "searchField"
let SEARCH_BAR_PLACEHOLDER_TEXT_KEY = "_placeholderLabel.textColor"
func customiseSearchBar() {
searchBar.isTranslucent = true
searchBar.backgroundImage = UIImage.imageWithColor(color: UIColor.yellow, size: CGSize(width: searchBar.frame.width,height: searchBar.frame.height))
//modify textfield font and color attributes
let textFieldSearchBar = searchBar.value(forKey: SEARCH_BAR_SEARCH_FIELD_KEY) as? UITextField
textFieldSearchBar?.backgroundColor = UIColor.yellow
textFieldSearchBar?.font = UIFont(name: "Helvetica Neue" as String, size: 18)
textFieldSearchBar?.setValue(UIColor.yellow, forKeyPath: SEARCH_BAR_PLACEHOLDER_TEXT_KEY)
}
extension UIImage {
class func imageWithColor(color: UIColor, size: CGSize) -> UIImage {
let rect: CGRect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
color.setFill()
UIRectFill(rect)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
This achieves the UI as per your requirement, let me know if you need more customization on this. This is in swift 3 by the way.

Apply CIFilters on UI elements

I want to apply an CIFilter on an UI element. I tried to apply it onto the views layer via the .filters member. However the filter won`t get applied.
Here's an approach: use UIGraphicsGetImageFromCurrentImageContext to generate a UIImage, apply the filter to that and overlay an image view containing the filtered image over your original component.
Here's a way to do that with a blur (taken from my blog):
Getting a blurred representation of a UIView is pretty simple: I need to begin an image context, use the view's layer's renderInContext method to render into the context and then get a UIImage from the context:
UIGraphicsBeginImageContextWithOptions(CGSize(width: frame.width, height: frame.height), false, 1)
layer.renderInContext(UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext();
Once I have the image populated, it's a fairly standard workflow to apply a Gaussian blur to it:
guard let blur = CIFilter(name: "CIGaussianBlur") else
{
return
}
blur.setValue(CIImage(image: image), forKey: kCIInputImageKey)
blur.setValue(blurRadius, forKey: kCIInputRadiusKey)
let ciContext = CIContext(options: nil)
let result = blur.valueForKey(kCIOutputImageKey) as! CIImage!
let boundingRect = CGRect(x: -blurRadius * 4,
y: -blurRadius * 4,
width: frame.width + (blurRadius * 8),
height: frame.height + (blurRadius * 8))
let cgImage = ciContext.createCGImage(result, fromRect: boundingRect)
let filteredImage = UIImage(CGImage: cgImage)
A blurred image will be larger than its input image, so I need to be explicit about the size I require in createCGImage.
The next step is to add a UIImageView to my view and hide all the other views. I've subclassed UIImageView to BlurOverlay so that when it comes to removing it, I can be sure I'm not removing an existing UIImageView:
let blurOverlay = BlurOverlay()
blurOverlay.frame = boundingRect
blurOverlay.image = filteredImage
subviews.forEach{ $0.hidden = true }
addSubview(blurOverlay)
When it comes to de-blurring, I want to ensure the last subview is one of my BlurOverlay remove it and unhide the existing views:
func unBlur()
{
if let blurOverlay = subviews.last as? BlurOverlay
{
blurOverlay.removeFromSuperview()
subviews.forEach{ $0.hidden = false }
}
}
Finally, to see if a UIView is currently blurred, I just need to see if its last subview is a BlurOverlay:
var isBlurred: Bool
{
return subviews.last is BlurOverlay
}